From 4f28d4eb5b2aaac3e66b4989e68c0e6f42782356 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 07:30:43 +0300 Subject: [PATCH 1/7] build(deps): bump io.zipkin.reporter2:zipkin-reporter-brave (#3579) Bumps [io.zipkin.reporter2:zipkin-reporter-brave](https://github.com/openzipkin/zipkin-reporter-java) from 3.5.0 to 3.5.3. - [Release notes](https://github.com/openzipkin/zipkin-reporter-java/releases) - [Changelog](https://github.com/openzipkin/zipkin-reporter-java/blob/master/RELEASE.md) - [Commits](https://github.com/openzipkin/zipkin-reporter-java/compare/3.5.0...3.5.3) --- updated-dependencies: - dependency-name: io.zipkin.reporter2:zipkin-reporter-brave dependency-version: 3.5.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- microservices-distributed-tracing/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microservices-distributed-tracing/pom.xml b/microservices-distributed-tracing/pom.xml index c7dddf0fa0b6..fa9e288879f1 100644 --- a/microservices-distributed-tracing/pom.xml +++ b/microservices-distributed-tracing/pom.xml @@ -52,7 +52,7 @@ io.zipkin.reporter2 zipkin-reporter-brave - 3.5.0 + 3.5.3 org.junit.jupiter From 5698dea49b11216c180725cb07c787a62dcdcb2f Mon Sep 17 00:00:00 2001 From: Sandhya <128058717+SandhyaDevadiga@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:09:07 +0530 Subject: [PATCH 2/7] feat: Add Fork/Join design pattern (#3227) (#3550) * Add fork-join pattern implementation * Add input validation for start > end in SumTask Signed-off-by: SandhyaDevadiga * Add fork-join module to parent pom.xml * Add missing assertThrows import in SumTaskTest * Format fork-join code with Spotless * Add AppTest to satisfy coverage requirements Signed-off-by: SandhyaDevadiga --------- Signed-off-by: SandhyaDevadiga --- fork-join/README.md | 161 ++++++++++++++++++ fork-join/pom.xml | 50 ++++++ .../main/java/com/iluwatar/forkjoin/App.java | 43 +++++ .../forkjoin/ForkJoinSumCalculator.java | 44 +++++ .../java/com/iluwatar/forkjoin/SumTask.java | 93 ++++++++++ .../java/com/iluwatar/forkjoin/AppTest.java | 11 ++ .../forkjoin/ForkJoinSumCalculatorTest.java | 56 ++++++ .../com/iluwatar/forkjoin/SumTaskTest.java | 85 +++++++++ pom.xml | 1 + 9 files changed, 544 insertions(+) create mode 100644 fork-join/README.md create mode 100644 fork-join/pom.xml create mode 100644 fork-join/src/main/java/com/iluwatar/forkjoin/App.java create mode 100644 fork-join/src/main/java/com/iluwatar/forkjoin/ForkJoinSumCalculator.java create mode 100644 fork-join/src/main/java/com/iluwatar/forkjoin/SumTask.java create mode 100644 fork-join/src/test/java/com/iluwatar/forkjoin/AppTest.java create mode 100644 fork-join/src/test/java/com/iluwatar/forkjoin/ForkJoinSumCalculatorTest.java create mode 100644 fork-join/src/test/java/com/iluwatar/forkjoin/SumTaskTest.java diff --git a/fork-join/README.md b/fork-join/README.md new file mode 100644 index 000000000000..b94af1de02ac --- /dev/null +++ b/fork-join/README.md @@ -0,0 +1,161 @@ +--- +title: "Fork/Join Pattern in Java: Parallel Divide-and-Conquer Processing" +shortTitle: Fork/Join +description: "Learn the Fork/Join design pattern in Java with real-world examples, class diagrams, and code samples. Understand how to split large tasks into parallel subtasks for improved performance." +category: Concurrency +language: en +tag: + - Performance + - Scalability + - Concurrency +--- + +## Also known as + +* Divide and Conquer Parallelism +* Work-Stealing Parallelism + +## Intent of Fork/Join Design Pattern + +The Fork/Join pattern recursively splits a large task into independent subtasks (fork), +processes them in parallel across multiple threads, and combines their results (join) to +produce a final outcome. It maximizes CPU utilization for computationally intensive problems. + +## Detailed Explanation of Fork/Join Pattern with Real-World Examples + +Real-world example + +> Imagine a large warehouse that needs to count all its inventory items across 100 aisles. +> Instead of one person counting every aisle sequentially, the manager divides the warehouse +> into sections and assigns a team of workers to count each section simultaneously. Once +> every section is counted, the manager collects all partial counts and sums them into the +> total inventory. This is the Fork/Join pattern: split the work, do it in parallel, merge +> the results. + +In plain words + +> Fork/Join splits a big problem into smaller pieces, solves each piece in parallel on +> separate threads, then combines all results back together. + +## Programmatic Example of Fork/Join Pattern in Java + +We demonstrate the pattern by computing the sum of a large array in parallel using Java's +built-in `ForkJoinPool` and `RecursiveTask`. + +The `SumTask` is a recursive task that splits the array when it's too large: + +```java +public class SumTask extends RecursiveTask { + + private static final int THRESHOLD = 1000; + private final long[] numbers; + private final int start; + private final int end; + + @Override + protected Long compute() { + int length = end - start; + + if (length <= THRESHOLD) { + // Base case: sum directly + long sum = 0; + for (int i = start; i < end; i++) { + sum += numbers[i]; + } + return sum; + } + + // Fork: split into two halves + int mid = start + length / 2; + SumTask leftTask = new SumTask(numbers, start, mid); + SumTask rightTask = new SumTask(numbers, mid, end); + + leftTask.fork(); // run left half asynchronously + long rightResult = rightTask.compute(); // compute right half here + long leftResult = leftTask.join(); // wait for left half + + // Join: combine results + return leftResult + rightResult; + } +} +``` + +The `ForkJoinSumCalculator` provides a clean API: + +```java +public class ForkJoinSumCalculator { + + private final ForkJoinPool pool; + + public ForkJoinSumCalculator() { + this.pool = ForkJoinPool.commonPool(); + } + + public long calculateSum(long[] numbers) { + SumTask task = new SumTask(numbers, 0, numbers.length); + return pool.invoke(task); + } +} +``` + +Running the example in `App`: + +```java +long[] numbers = LongStream.rangeClosed(1, 10_000_000).toArray(); +ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); +long result = calculator.calculateSum(numbers); +System.out.println("Fork/Join sum: " + result); +``` + +Program output: + +``` +Fork/Join sum: 50000005000000 +Expected sum: 50000005000000 +Correct: true +Time taken: 45 ms +Available processors: 8 +``` + +## When to Use the Fork/Join Pattern in Java + +* When you have a large, CPU-intensive task that can be divided into independent subtasks. +* When the subtasks are roughly the same size and don't depend on each other. +* When you want to utilize multiple CPU cores without manually managing threads. +* When the problem naturally fits a divide-and-conquer strategy (e.g., sorting, searching, + numerical computation). + +## When NOT to Use Fork/Join + +* For I/O-bound tasks (network calls, file reads) — use virtual threads or async I/O instead. +* When subtasks are too small — the overhead of forking exceeds the benefit. +* When tasks have dependencies on each other and cannot run independently. + +## Benefits and Trade-offs of Fork/Join Pattern + +Benefits: + +* Maximizes CPU utilization through work-stealing algorithm. +* Scales automatically with the number of available processors. +* Built into Java's standard library (`java.util.concurrent`) — no external dependencies. +* Clean recursive decomposition makes the code readable and maintainable. + +Trade-offs: + +* Overhead from task creation and thread management for very small problems. +* Requires tasks to be independent — shared mutable state introduces bugs. +* Choosing an appropriate threshold requires tuning for optimal performance. +* Debugging parallel code is inherently harder than sequential code. + +## Related Java Design Patterns + +* [Divide and Conquer](https://java-design-patterns.com/patterns/divide-and-conquer/): + Fork/Join is the parallel execution variant of the classic divide-and-conquer strategy. +* [Thread Pool](https://java-design-patterns.com/patterns/thread-pool/): Fork/Join uses a + specialized pool with work-stealing semantics. + +## References + +* [Java Documentation for ForkJoinPool](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ForkJoinPool.html) +* [Java Concurrency in Practice — Brian Goetz](https://amzn.to/4aRMruW) +* [Java Documentation for RecursiveTask](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/RecursiveTask.html) diff --git a/fork-join/pom.xml b/fork-join/pom.xml new file mode 100644 index 000000000000..23958727728a --- /dev/null +++ b/fork-join/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + fork-join + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.forkjoin.App + + + + + + + + + + diff --git a/fork-join/src/main/java/com/iluwatar/forkjoin/App.java b/fork-join/src/main/java/com/iluwatar/forkjoin/App.java new file mode 100644 index 000000000000..a53bdcec6de7 --- /dev/null +++ b/fork-join/src/main/java/com/iluwatar/forkjoin/App.java @@ -0,0 +1,43 @@ +package com.iluwatar.forkjoin; + +import java.util.stream.LongStream; + +/** + * The Fork/Join pattern is a concurrency design pattern that splits a large task into smaller + * subtasks (fork), processes them in parallel, and then combines the results (join). + * + *

In Java, this pattern is implemented using {@link java.util.concurrent.ForkJoinPool} and + * {@link java.util.concurrent.RecursiveTask}. Worker threads in the pool use a work-stealing + * algorithm — idle threads take tasks from busy threads — maximizing CPU utilization. + * + *

In this example, we demonstrate the pattern by computing the sum of a large array in parallel. + * The array is recursively split in half until each piece is small enough to sum directly, then + * results are combined back up. + */ +public final class App { + + private App() {} + + /** + * @param args command line arguments, not used + */ + public static void main(String[] args) { + // Create an array of 10 million numbers: [1, 2, 3, ..., 10_000_000] + long[] numbers = LongStream.rangeClosed(1, 10_000_000).toArray(); + + // Calculate sum using Fork/Join + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); + long startTime = System.currentTimeMillis(); + long result = calculator.calculateSum(numbers); + long endTime = System.currentTimeMillis(); + + // The expected sum of 1 to N is N*(N+1)/2 + long expected = 10_000_000L * 10_000_001L / 2; + + System.out.println("Fork/Join sum: " + result); + System.out.println("Expected sum: " + expected); + System.out.println("Correct: " + (result == expected)); + System.out.println("Time taken: " + (endTime - startTime) + " ms"); + System.out.println("Available processors: " + Runtime.getRuntime().availableProcessors()); + } +} diff --git a/fork-join/src/main/java/com/iluwatar/forkjoin/ForkJoinSumCalculator.java b/fork-join/src/main/java/com/iluwatar/forkjoin/ForkJoinSumCalculator.java new file mode 100644 index 000000000000..0e9dff32cda8 --- /dev/null +++ b/fork-join/src/main/java/com/iluwatar/forkjoin/ForkJoinSumCalculator.java @@ -0,0 +1,44 @@ +package com.iluwatar.forkjoin; + +import java.util.concurrent.ForkJoinPool; + +/** + * ForkJoinSumCalculator provides a convenient API to sum an array of numbers using the Fork/Join + * framework. It creates a {@link ForkJoinPool}, submits a {@link SumTask}, and returns the computed + * sum. + * + *

The pool manages a set of worker threads that process subtasks in parallel. Idle threads can + * "steal" work from busy threads, maximizing CPU utilization. + */ +public class ForkJoinSumCalculator { + + private final ForkJoinPool pool; + + /** Creates a calculator using the common ForkJoinPool (uses all available CPU cores). */ + public ForkJoinSumCalculator() { + this.pool = ForkJoinPool.commonPool(); + } + + /** + * Creates a calculator with a specific number of threads. + * + * @param parallelism the number of worker threads to use + */ + public ForkJoinSumCalculator(int parallelism) { + this.pool = new ForkJoinPool(parallelism); + } + + /** + * Calculates the sum of all elements in the array using Fork/Join parallelism. + * + * @param numbers the array of numbers to sum + * @return the total sum of all elements + */ + public long calculateSum(long[] numbers) { + if (numbers == null || numbers.length == 0) { + return 0; + } + SumTask task = new SumTask(numbers, 0, numbers.length); + return pool.invoke(task); + } +} diff --git a/fork-join/src/main/java/com/iluwatar/forkjoin/SumTask.java b/fork-join/src/main/java/com/iluwatar/forkjoin/SumTask.java new file mode 100644 index 000000000000..acbd5f0516b7 --- /dev/null +++ b/fork-join/src/main/java/com/iluwatar/forkjoin/SumTask.java @@ -0,0 +1,93 @@ +package com.iluwatar.forkjoin; + +import java.util.concurrent.RecursiveTask; + +/** + * SumTask demonstrates the Fork/Join pattern by recursively splitting an array summation problem + * into smaller subtasks until each subtask is small enough to compute directly. + * + *

How it works: If the portion of the array is smaller than THRESHOLD, sum it in a simple loop. + * Otherwise, split the array in half, fork one half to run in parallel, compute the other half in + * the current thread, and then join the results. This approach utilizes multiple CPU cores to + * perform the summation significantly faster than a single-threaded loop for large arrays. + */ +public class SumTask extends RecursiveTask { + + /** + * If the number of elements to process is at or below this threshold, the task computes the sum + * directly instead of splitting further. + */ + private static final int THRESHOLD = 1000; + + private final long[] numbers; + private final int start; + private final int end; + + /** + * Creates a task to sum elements of the given array from index {@code start} (inclusive) to index + * {@code end} (exclusive). + * + * @param numbers the array of numbers to sum + * @param start the starting index (inclusive) + * @param end the ending index (exclusive) + */ + public SumTask(long[] numbers, int start, int end) { + if (start > end) { + throw new IllegalArgumentException( + "start (" + start + ") must not be greater than end (" + end + ")"); + } + this.numbers = numbers; + this.start = start; + this.end = end; + } + + /** + * The main computation method. This is where the fork/join magic happens. + * + * @return the sum of elements from start to end + */ + @Override + protected Long compute() { + int length = end - start; + + // BASE CASE: if the chunk is small enough, just sum directly + if (length <= THRESHOLD) { + return computeDirectly(); + } + + // FORK: split the task into two halves + int mid = start + length / 2; + + // Create subtask for the left half + SumTask leftTask = new SumTask(numbers, start, mid); + + // Create subtask for the right half + SumTask rightTask = new SumTask(numbers, mid, end); + + // Fork the left task — it will run in a separate thread + leftTask.fork(); + + // Compute the right task in the current thread (no need to fork both) + long rightResult = rightTask.compute(); + + // JOIN: wait for the left task to finish and get its result + long leftResult = leftTask.join(); + + // Combine the results from both halves + return leftResult + rightResult; + } + + /** + * Computes the sum directly using a simple loop. This is used when the chunk size is at or below + * the threshold — no further splitting needed. + * + * @return the sum of elements in the range [start, end) + */ + private long computeDirectly() { + long sum = 0; + for (int i = start; i < end; i++) { + sum += numbers[i]; + } + return sum; + } +} diff --git a/fork-join/src/test/java/com/iluwatar/forkjoin/AppTest.java b/fork-join/src/test/java/com/iluwatar/forkjoin/AppTest.java new file mode 100644 index 000000000000..c66f8610cc55 --- /dev/null +++ b/fork-join/src/test/java/com/iluwatar/forkjoin/AppTest.java @@ -0,0 +1,11 @@ +package com.iluwatar.forkjoin; + +import org.junit.jupiter.api.Test; + +class AppTest { + + @Test + void shouldExecuteWithoutException() { + App.main(new String[] {}); + } +} diff --git a/fork-join/src/test/java/com/iluwatar/forkjoin/ForkJoinSumCalculatorTest.java b/fork-join/src/test/java/com/iluwatar/forkjoin/ForkJoinSumCalculatorTest.java new file mode 100644 index 000000000000..6af56dfc38e1 --- /dev/null +++ b/fork-join/src/test/java/com/iluwatar/forkjoin/ForkJoinSumCalculatorTest.java @@ -0,0 +1,56 @@ +package com.iluwatar.forkjoin; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.LongStream; +import org.junit.jupiter.api.Test; + +class ForkJoinSumCalculatorTest { + + @Test + void shouldReturnZeroForNullArray() { + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); + + assertEquals(0L, calculator.calculateSum(null)); + } + + @Test + void shouldReturnZeroForEmptyArray() { + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); + + assertEquals(0L, calculator.calculateSum(new long[0])); + } + + @Test + void shouldCalculateSumOfSmallArray() { + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); + long[] numbers = {10, 20, 30, 40, 50}; + + long result = calculator.calculateSum(numbers); + + assertEquals(150L, result); + } + + @Test + void shouldCalculateSumOfLargeArray() { + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); + long[] numbers = LongStream.rangeClosed(1, 100_000).toArray(); + + long result = calculator.calculateSum(numbers); + + long expected = 100_000L * 100_001L / 2; + assertEquals(expected, result); + } + + @Test + void shouldWorkWithCustomParallelism() { + // Use only 2 threads + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(2); + long[] numbers = LongStream.rangeClosed(1, 50_000).toArray(); + + long result = calculator.calculateSum(numbers); + + long expected = 50_000L * 50_001L / 2; + assertEquals(expected, result); + } +} diff --git a/fork-join/src/test/java/com/iluwatar/forkjoin/SumTaskTest.java b/fork-join/src/test/java/com/iluwatar/forkjoin/SumTaskTest.java new file mode 100644 index 000000000000..03df6a6bc268 --- /dev/null +++ b/fork-join/src/test/java/com/iluwatar/forkjoin/SumTaskTest.java @@ -0,0 +1,85 @@ +package com.iluwatar.forkjoin; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.ForkJoinPool; +import java.util.stream.LongStream; +import org.junit.jupiter.api.Test; + +class SumTaskTest { + + @Test + void shouldSumSmallArrayDirectly() { + // Array smaller than threshold — should compute without forking + long[] numbers = {1, 2, 3, 4, 5}; + SumTask task = new SumTask(numbers, 0, numbers.length); + + long result = ForkJoinPool.commonPool().invoke(task); + + assertEquals(15L, result); + } + + @Test + void shouldSumLargeArrayUsingForkJoin() { + // Array larger than threshold — will fork into subtasks + long[] numbers = LongStream.rangeClosed(1, 10_000).toArray(); + SumTask task = new SumTask(numbers, 0, numbers.length); + + long result = ForkJoinPool.commonPool().invoke(task); + + // Sum of 1 to N = N*(N+1)/2 + long expected = 10_000L * 10_001L / 2; + assertEquals(expected, result); + } + + @Test + void shouldSumPartialRange() { + // Sum only a portion of the array (indices 2 to 5) + long[] numbers = {10, 20, 30, 40, 50, 60}; + SumTask task = new SumTask(numbers, 2, 5); + + long result = ForkJoinPool.commonPool().invoke(task); + + // 30 + 40 + 50 = 120 + assertEquals(120L, result); + } + + @Test + void shouldReturnZeroForEmptyRange() { + long[] numbers = {1, 2, 3}; + SumTask task = new SumTask(numbers, 1, 1); // start == end, empty range + + long result = ForkJoinPool.commonPool().invoke(task); + + assertEquals(0L, result); + } + + @Test + void shouldHandleSingleElement() { + long[] numbers = {42}; + SumTask task = new SumTask(numbers, 0, 1); + + long result = ForkJoinPool.commonPool().invoke(task); + + assertEquals(42L, result); + } + + @Test + void shouldProduceCorrectResultForMillionElements() { + long[] numbers = LongStream.rangeClosed(1, 1_000_000).toArray(); + SumTask task = new SumTask(numbers, 0, numbers.length); + + long result = ForkJoinPool.commonPool().invoke(task); + + long expected = 1_000_000L * 1_000_001L / 2; + assertEquals(expected, result); + } + + @Test + void shouldThrowExceptionWhenStartGreaterThanEnd() { + long[] numbers = {1, 2, 3, 4, 5}; + + assertThrows(IllegalArgumentException.class, () -> new SumTask(numbers, 4, 2)); + } +} diff --git a/pom.xml b/pom.xml index 46cec4ee61c4..9a4ba4de8ca2 100644 --- a/pom.xml +++ b/pom.xml @@ -139,6 +139,7 @@ fluent-interface flux flyweight + fork-join front-controller function-composition game-loop From aa11f6b6be465fc29bda4334678d2168ff01400e Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 07:39:42 +0300 Subject: [PATCH 3/7] docs: add SandhyaDevadiga as a contributor for code (#3585) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index df0e170f068f..83b0ddaf8072 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3773,6 +3773,15 @@ "contributions": [ "code" ] + }, + { + "login": "SandhyaDevadiga", + "name": "Sandhya", + "avatar_url": "https://avatars.githubusercontent.com/u/128058717?v=4", + "profile": "https://github.com/SandhyaDevadiga", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 6, diff --git a/README.md b/README.md index f0e461c0fb76..c5c284f6b29f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=iluwatar_java-design-patterns&metric=coverage)](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns) [![Join the chat at https://gitter.im/iluwatar/java-design-patterns](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-414-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-415-orange.svg?style=flat-square)](#contributors-)
@@ -613,6 +613,9 @@ This project is licensed under the terms of the MIT license. Anvesh Srivastava
Anvesh Srivastava

💻 Mukul Howale
Mukul Howale

💻 + + Sandhya
Sandhya

💻 + From b0c34db084dc3309087d10e77775662528bfcbc7 Mon Sep 17 00:00:00 2001 From: dev-ikae <98100047+devikae@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:47:35 +0900 Subject: [PATCH 4/7] feat: Implement Write-Ahead Log (WAL) pattern (#3576) (#3582) * feat: Implement Write-Ahead Log (WAL) pattern (#3576) * test: Add exception handling tests and verify coverage --- pom.xml | 1 + write-ahead-log/README.md | 217 ++++++++++++++++++ write-ahead-log/pom.xml | 87 +++++++ .../java/com/iluwatar/writeaheadlog/App.java | 82 +++++++ .../iluwatar/writeaheadlog/DatabaseStore.java | 131 +++++++++++ .../com/iluwatar/writeaheadlog/LogEntry.java | 83 +++++++ .../iluwatar/writeaheadlog/OperationType.java | 33 +++ .../iluwatar/writeaheadlog/WriteAheadLog.java | 134 +++++++++++ .../com/iluwatar/writeaheadlog/AppTest.java | 38 +++ .../writeaheadlog/DatabaseStoreTest.java | 96 ++++++++ .../iluwatar/writeaheadlog/LogEntryTest.java | 80 +++++++ .../writeaheadlog/WriteAheadLogTest.java | 134 +++++++++++ 12 files changed, 1116 insertions(+) create mode 100644 write-ahead-log/README.md create mode 100644 write-ahead-log/pom.xml create mode 100644 write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/App.java create mode 100644 write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/DatabaseStore.java create mode 100644 write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/LogEntry.java create mode 100644 write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/OperationType.java create mode 100644 write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/WriteAheadLog.java create mode 100644 write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/AppTest.java create mode 100644 write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/DatabaseStoreTest.java create mode 100644 write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/LogEntryTest.java create mode 100644 write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/WriteAheadLogTest.java diff --git a/pom.xml b/pom.xml index 9a4ba4de8ca2..4ed64e643219 100644 --- a/pom.xml +++ b/pom.xml @@ -253,6 +253,7 @@ view-helper virtual-proxy visitor + write-ahead-log backpressure actor-model rate-limiting-pattern diff --git a/write-ahead-log/README.md b/write-ahead-log/README.md new file mode 100644 index 000000000000..45caab13cadf --- /dev/null +++ b/write-ahead-log/README.md @@ -0,0 +1,217 @@ +--- +title: "Write-Ahead Log (WAL) Pattern in Java: Ensuring Data Durability and Crash Recovery" +shortTitle: Write-Ahead Log +description: "Learn about the Write-Ahead Log (WAL) design pattern in Java. Discover how append-only logging guarantees data durability and fast crash recovery in database engines and distributed systems." +category: Data Access +language: en +tag: + - Data access + - Storage + - Fault tolerance + - Transactions + - Performance +--- + +## Also known as + +* Append-Only Log +* Redo Log +* Journaling + +## Intent of Write-Ahead Log Pattern + +The Write-Ahead Log (WAL) design pattern ensures data durability and system recoverability in database engines, distributed consensus protocols, and transactional systems. It enforces a strict order of operations where any state mutation (e.g., insert, update, delete) must be written sequentially to an append-only log file on stable storage (disk) before it is applied to the main database state or in-memory storage structures. + +## Detailed Explanation of Write-Ahead Log Pattern with Real-World Examples + +Real-world example + +> Imagine an accountant managing a company's ledger. Before modifying the main financial summary balance sheets, the accountant immediately records every incoming transaction line-by-line into a sequential physical logbook. If power cuts out mid-day or the summary balance sheets are damaged, the accountant can re-open the physical logbook, replay every recorded entry from the beginning, and perfectly recalculate the final financial state. + +In plain words + +> Write-Ahead Log guarantees that no state mutation is lost during sudden system crashes by writing changes to a fast append-only disk log file before updating the in-memory store. + +Wikipedia says + +> In computer science, write-ahead logging (WAL) is a family of techniques for providing atomicity and durability (two of the ACID properties) in database systems. In a system using WAL, all modifications are written to a log before they are applied. Usually both redo and undo information are stored in the log. + +Class Diagram + +```mermaid +classDiagram + class OperationType { + <> + SET + DELETE + CHECKPOINT + } + + class LogEntry { + -long sequenceNumber + -OperationType type + -String key + -String value + +toLogString() String + +fromLogString(String line)$ LogEntry + } + + class WriteAheadLog { + -File logFile + -AtomicLong sequenceNumberCounter + +append(OperationType type, String key, String value) LogEntry + +readAll() List~LogEntry~ + +clear() void + } + + class DatabaseStore { + -WriteAheadLog wal + -Map~String, String~ memTable + +put(String key, String value) void + +delete(String key) void + +get(String key) String + +checkpoint() void + +simulateCrash() void + +recover() void + } + + DatabaseStore --> WriteAheadLog + WriteAheadLog --> LogEntry + LogEntry --> OperationType +``` + +## Programmatic Example of Write-Ahead Log Pattern in Java + +The `WriteAheadLog` class manages append-only sequential writes to disk: + +```java +public class WriteAheadLog { + private final File logFile; + private final AtomicLong sequenceNumberCounter = new AtomicLong(0); + + public synchronized LogEntry append(OperationType type, String key, String value) throws IOException { + long nextSeq = sequenceNumberCounter.incrementAndGet(); + LogEntry entry = new LogEntry(nextSeq, type, key, value); + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(logFile, true))) { + writer.write(entry.toLogString()); + writer.newLine(); + writer.flush(); + } + return entry; + } +} +``` + +The `DatabaseStore` class coordinates writing to the log before modifying its in-memory `MemTable`: + +```java +public class DatabaseStore { + private final WriteAheadLog wal; + private final Map memTable = new HashMap<>(); + + public synchronized void put(String key, String value) throws IOException { + wal.append(OperationType.SET, key, value); + memTable.put(key, value); + } + + public synchronized void delete(String key) throws IOException { + wal.append(OperationType.DELETE, key, null); + memTable.remove(key); + } + + public synchronized void recover() { + memTable.clear(); + List entries = wal.readAll(); + for (LogEntry entry : entries) { + if (entry.getType() == OperationType.SET) { + memTable.put(entry.getKey(), entry.getValue()); + } else if (entry.getType() == OperationType.DELETE) { + memTable.remove(entry.getKey()); + } + } + } +} +``` + +The `App` class demonstrates initialization, writes, crash simulation, and WAL recovery: + +```java +@Slf4j +public class App { + public static void main(String[] args) { + try { + File logFile = File.createTempFile("wal_demo", ".log"); + WriteAheadLog wal = new WriteAheadLog(logFile); + DatabaseStore store = new DatabaseStore(wal); + + store.put("user:101", "Alice"); + store.put("user:102", "Bob"); + store.delete("user:103"); + + // Simulating system crash where in-memory state is lost + store.simulateCrash(); + + // System reboot & recovery from WAL log replay + store.recover(); + + LOGGER.info("MemTable post recovery: {}", store.getMemTableSnapshot()); + } catch (IOException e) { + LOGGER.error("Error running WAL demo", e); + } + } +} +``` + +Program output: + +```text +15:45:00.100 [main] INFO com.iluwatar.writeaheadlog.App -- === 1. Initializing Storage Engine with WAL === +15:45:00.105 [main] INFO com.iluwatar.writeaheadlog.WriteAheadLog -- WAL Entry appended & flushed to disk: LogEntry(sequenceNumber=1, type=SET, key=user:101, value=Alice) +15:45:00.106 [main] INFO com.iluwatar.writeaheadlog.DatabaseStore -- Applied SET operation to MemTable: user:101 = Alice +15:45:00.107 [main] INFO com.iluwatar.writeaheadlog.App -- === 3. Simulating Unexpected System Crash === +15:45:00.108 [main] INFO com.iluwatar.writeaheadlog.DatabaseStore -- !!! SIMULATED SYSTEM CRASH: In-memory MemTable has been wiped !!! +15:45:00.109 [main] INFO com.iluwatar.writeaheadlog.App -- === 4. System Restart & Recovery from WAL === +15:45:00.110 [main] INFO com.iluwatar.writeaheadlog.DatabaseStore -- Starting recovery process from WAL... +15:45:00.112 [main] INFO com.iluwatar.writeaheadlog.DatabaseStore -- Recovery completed. Replayed 5 log entries into MemTable. +15:45:00.113 [main] INFO com.iluwatar.writeaheadlog.App -- MemTable snapshot post recovery: {user:101=Alice, user:102=Bob Smith} +``` + +## When to Use the Write-Ahead Log Pattern in Java + +* Building storage engines or key-value data stores requiring ACID durability guarantees. +* Implementing fault-tolerant distributed consensus protocols (e.g., Raft, Paxos). +* System architectures where random disk I/O is expensive, allowing sequential append-only writes for maximum throughput. +* Message brokers or event streams requiring replayability after failure. + +## Real-World Applications of Write-Ahead Log Pattern in Java + +* **PostgreSQL / MySQL (InnoDB):** Uses WAL / Redo Log for crash recovery and replication. +* **SQLite:** Write-Ahead Logging mode for concurrency and atomic commits. +* **Apache Cassandra / RocksDB:** Appends mutations to CommitLog / WAL before MemTable updates. +* **Apache Kafka / Raft:** Log replication across distributed nodes for consensus and state machine replication. + +## Benefits and Trade-offs of Write-Ahead Log Pattern + +Benefits: + +* **High Performance:** Sequential disk writes are significantly faster than random disk updates (e.g., updating B-Trees directly). +* **Durability & Fault Tolerance:** Guarantees no committed transaction is lost during sudden system crashes. +* **Simplicity of Recovery:** Replaying ordered log records deterministically restores the exact last-known state. + +Trade-offs: + +* **Storage Overhead:** Log files grow over time, requiring periodic checkpointing and log truncation. +* **Recovery Time:** Large log files without checkpoints can lead to slow startup/recovery times. + +## Related Java Design Patterns + +* [Event Sourcing](https://java-design-patterns.com/patterns/event-sourcing/): Captures state mutations as a sequence of events, similar to log replay. +* [Command](https://java-design-patterns.com/patterns/command/): Encapsulates requests as objects, which can be serialized into WAL entries. +* [Memento](https://java-design-patterns.com/patterns/memento/): Stores state snapshots (checkpoints) to truncate logs. + +## References and Credits + +* [Designing Data-Intensive Applications (Martin Kleppmann)](https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/) +* [PostgreSQL Documentation: Write-Ahead Logging (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) +* [Raft Consensus Algorithm Paper](https://raft.github.io/) diff --git a/write-ahead-log/pom.xml b/write-ahead-log/pom.xml new file mode 100644 index 000000000000..adb766953a47 --- /dev/null +++ b/write-ahead-log/pom.xml @@ -0,0 +1,87 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + write-ahead-log + 1.26.0-SNAPSHOT + write-ahead-log + http://maven.apache.org + + UTF-8 + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.projectlombok + lombok + ${lombok.version} + provided + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.writeaheadlog.App + + + + + + + + + diff --git a/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/App.java b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/App.java new file mode 100644 index 000000000000..e8e9fae2f763 --- /dev/null +++ b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/App.java @@ -0,0 +1,82 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import java.io.File; +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; + +/** + * Main application class demonstrating the Write-Ahead Log (WAL) design pattern. + * + *

The WAL pattern guarantees durability by ensuring every mutation (SET, DELETE) is written to a + * persistent append-only log file on disk BEFORE updating in-memory state. If the system crashes + * unexpectedly, replaying log entries from the WAL file restores state. + */ +@Slf4j +public class App { + + /** + * Application entry point. + * + * @param args command line arguments + */ + public static void main(String[] args) { + try { + File logFile = File.createTempFile("wal_demo", ".log"); + logFile.deleteOnExit(); + + LOGGER.info( + "=== 1. Initializing Storage Engine with WAL at {} ===", logFile.getAbsolutePath()); + WriteAheadLog wal = new WriteAheadLog(logFile); + DatabaseStore store = new DatabaseStore(wal); + + LOGGER.info("=== 2. Performing Data Operations (Write-Ahead Logging) ==="); + store.put("user:101", "Alice"); + store.put("user:102", "Bob"); + store.put("user:103", "Charlie"); + store.put("user:102", "Bob Smith"); + store.delete("user:103"); + store.checkpoint(); + + LOGGER.info("MemTable snapshot before crash: {}", store.getMemTableSnapshot()); + + LOGGER.info("=== 3. Simulating Unexpected System Crash ==="); + store.simulateCrash(); + LOGGER.info("MemTable snapshot after crash: {}", store.getMemTableSnapshot()); + + LOGGER.info("=== 4. System Restart & Recovery from WAL ==="); + store.recover(); + LOGGER.info("MemTable snapshot post recovery: {}", store.getMemTableSnapshot()); + + if (logFile.exists()) { + logFile.delete(); + } + } catch (IOException e) { + LOGGER.error("An error occurred during WAL demonstration: {}", e.getMessage(), e); + } + } +} diff --git a/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/DatabaseStore.java b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/DatabaseStore.java new file mode 100644 index 000000000000..dd6d94fce7aa --- /dev/null +++ b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/DatabaseStore.java @@ -0,0 +1,131 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Storage engine demonstrating Write-Ahead Log (WAL) pattern. All mutations are logged and flushed + * to persistent WAL storage before modifying the in-memory MemTable. + */ +@Slf4j +public class DatabaseStore { + + @Getter private final WriteAheadLog wal; + private final Map memTable = new HashMap<>(); + + /** + * Constructs DatabaseStore with the specified WriteAheadLog. + * + * @param wal persistent write-ahead log manager + */ + public DatabaseStore(WriteAheadLog wal) { + this.wal = wal; + } + + /** + * Stores a key-value pair. First writes to WAL on disk, then updates the in-memory MemTable. + * + * @param key target entry key + * @param value target entry value + * @throws IOException if writing to WAL fails + */ + public synchronized void put(String key, String value) throws IOException { + wal.append(OperationType.SET, key, value); + memTable.put(key, value); + LOGGER.info("Applied SET operation to MemTable: {} = {}", key, value); + } + + /** + * Removes a key-value pair. First writes DELETE operation to WAL on disk, then updates the + * in-memory MemTable. + * + * @param key target entry key to delete + * @throws IOException if writing to WAL fails + */ + public synchronized void delete(String key) throws IOException { + wal.append(OperationType.DELETE, key, null); + memTable.remove(key); + LOGGER.info("Applied DELETE operation to MemTable: {}", key); + } + + /** + * Retrieves value associated with key from the in-memory MemTable. + * + * @param key key to lookup + * @return value or null if non-existent + */ + public synchronized String get(String key) { + return memTable.get(key); + } + + /** + * Returns an unmodifiable view of current in-memory MemTable state. + * + * @return unmodifiable map of stored data + */ + public synchronized Map getMemTableSnapshot() { + return Collections.unmodifiableMap(new HashMap<>(memTable)); + } + + /** + * Writes a CHECKPOINT log record. + * + * @throws IOException if writing to WAL fails + */ + public synchronized void checkpoint() throws IOException { + wal.append(OperationType.CHECKPOINT, null, null); + LOGGER.info("Checkpoint written to WAL."); + } + + /** Simulates a system crash or power outage where in-memory state is wiped. */ + public synchronized void simulateCrash() { + memTable.clear(); + LOGGER.info("!!! SIMULATED SYSTEM CRASH: In-memory MemTable has been wiped !!!"); + } + + /** Replays log entries from persistent Write-Ahead Log to fully recover in-memory state. */ + public synchronized void recover() { + LOGGER.info("Starting recovery process from WAL..."); + memTable.clear(); + List entries = wal.readAll(); + + for (LogEntry entry : entries) { + if (entry.getType() == OperationType.SET) { + memTable.put(entry.getKey(), entry.getValue()); + } else if (entry.getType() == OperationType.DELETE) { + memTable.remove(entry.getKey()); + } + } + LOGGER.info("Recovery completed. Replayed {} log entries into MemTable.", entries.size()); + } +} diff --git a/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/LogEntry.java b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/LogEntry.java new file mode 100644 index 000000000000..7ae212c8337c --- /dev/null +++ b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/LogEntry.java @@ -0,0 +1,83 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** Represents a single record entry in the Write-Ahead Log. */ +@Getter +@AllArgsConstructor +@EqualsAndHashCode +@ToString +public class LogEntry { + + private static final String DELIMITER = "|"; + + private final long sequenceNumber; + private final OperationType type; + private final String key; + private final String value; + + /** + * Serializes the LogEntry to a delimited string format suitable for append-only logging. + * + * @return delimited log line representation + */ + public String toLogString() { + return sequenceNumber + + DELIMITER + + type + + DELIMITER + + (key != null ? key : "") + + DELIMITER + + (value != null ? value : ""); + } + + /** + * Deserializes a delimited string line into a LogEntry instance. + * + * @param line serialized log line + * @return parsed LogEntry + * @throws IllegalArgumentException if the log line format is invalid + */ + public static LogEntry fromLogString(String line) { + if (line == null || line.isBlank()) { + throw new IllegalArgumentException("Log line cannot be null or blank"); + } + String[] parts = line.split("\\|", -1); + if (parts.length < 4) { + throw new IllegalArgumentException("Invalid log line format: " + line); + } + long sequenceNumber = Long.parseLong(parts[0]); + OperationType type = OperationType.valueOf(parts[1]); + String key = parts[2].isEmpty() ? null : parts[2]; + String value = parts[3].isEmpty() ? null : parts[3]; + return new LogEntry(sequenceNumber, type, key, value); + } +} diff --git a/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/OperationType.java b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/OperationType.java new file mode 100644 index 000000000000..33e06111bdc4 --- /dev/null +++ b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/OperationType.java @@ -0,0 +1,33 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +/** Enumeration representing the type of operations logged in the Write-Ahead Log. */ +public enum OperationType { + SET, + DELETE, + CHECKPOINT +} diff --git a/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/WriteAheadLog.java b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/WriteAheadLog.java new file mode 100644 index 000000000000..4ea2044ff2fc --- /dev/null +++ b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/WriteAheadLog.java @@ -0,0 +1,134 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Manages sequential append-only logging to persistent disk storage. Ensures state changes are + * flushed to stable storage before in-memory updates. + */ +@Slf4j +public class WriteAheadLog { + + @Getter private final File logFile; + private final AtomicLong sequenceNumberCounter = new AtomicLong(0); + + /** + * Initializes WriteAheadLog with target log file. If log file exists, calculates the initial + * sequence number from existing entries. + * + * @param logFile file to use for append-only log entries + */ + public WriteAheadLog(File logFile) { + this.logFile = logFile; + initSequenceNumber(); + } + + private void initSequenceNumber() { + if (logFile.exists()) { + List existingEntries = readAll(); + if (!existingEntries.isEmpty()) { + long maxSeq = existingEntries.get(existingEntries.size() - 1).getSequenceNumber(); + sequenceNumberCounter.set(maxSeq); + } + } + } + + /** + * Appends an entry to the log file and flushes to ensure persistence. + * + * @param type operation type (SET, DELETE, CHECKPOINT) + * @param key operation target key + * @param value operation target value + * @return recorded LogEntry + * @throws IOException if writing to persistent storage fails + */ + public synchronized LogEntry append(OperationType type, String key, String value) + throws IOException { + long nextSeq = sequenceNumberCounter.incrementAndGet(); + LogEntry entry = new LogEntry(nextSeq, type, key, value); + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(logFile, true))) { + writer.write(entry.toLogString()); + writer.newLine(); + writer.flush(); + } + LOGGER.info("WAL Entry appended & flushed to disk: {}", entry); + return entry; + } + + /** + * Reads all log entries sequentially from the persistent log file. + * + * @return list of parsed LogEntries in sequential order + */ + public synchronized List readAll() { + List entries = new ArrayList<>(); + if (!logFile.exists()) { + return entries; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.isBlank()) { + entries.add(LogEntry.fromLogString(line)); + } + } + } catch (IOException e) { + LOGGER.error("Failed to read log entries from WAL file: {}", logFile.getAbsolutePath(), e); + } + return entries; + } + + /** + * Clears the log file and resets the sequence number counter. Typically invoked after a + * successful checkpoint. + */ + public synchronized void clear() { + if (logFile.exists()) { + try { + Files.delete(logFile.toPath()); + sequenceNumberCounter.set(0); + LOGGER.info("WAL log cleared successfully."); + } catch (IOException e) { + LOGGER.error("Failed to clear WAL file: {}", logFile.getAbsolutePath(), e); + } + } + } +} diff --git a/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/AppTest.java b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/AppTest.java new file mode 100644 index 000000000000..b3bdbe3360ba --- /dev/null +++ b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/AppTest.java @@ -0,0 +1,38 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +class AppTest { + + @Test + void testAppMainExecutesWithoutExceptions() { + assertDoesNotThrow(() -> App.main(new String[0])); + } +} diff --git a/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/DatabaseStoreTest.java b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/DatabaseStoreTest.java new file mode 100644 index 000000000000..08288547348b --- /dev/null +++ b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/DatabaseStoreTest.java @@ -0,0 +1,96 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DatabaseStoreTest { + + private File tempFile; + private WriteAheadLog wal; + private DatabaseStore store; + + @BeforeEach + void setUp() throws IOException { + tempFile = File.createTempFile("db_store_test", ".log"); + wal = new WriteAheadLog(tempFile); + store = new DatabaseStore(wal); + } + + @AfterEach + void tearDown() { + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); + } + } + + @Test + void testPutAndGet() throws IOException { + store.put("key1", "val1"); + assertEquals("val1", store.get("key1")); + } + + @Test + void testDelete() throws IOException { + store.put("key1", "val1"); + assertEquals("val1", store.get("key1")); + + store.delete("key1"); + assertNull(store.get("key1")); + } + + @Test + void testSimulateCrashAndRecovery() throws IOException { + store.put("key1", "val1"); + store.put("key2", "val2"); + store.put("key1", "val1_updated"); + store.delete("key2"); + store.checkpoint(); + + Map beforeCrashSnapshot = store.getMemTableSnapshot(); + assertEquals("val1_updated", beforeCrashSnapshot.get("key1")); + assertNull(beforeCrashSnapshot.get("key2")); + + store.simulateCrash(); + assertNull(store.get("key1")); + assertNull(store.get("key2")); + assertTrue(store.getMemTableSnapshot().isEmpty()); + + store.recover(); + Map recoveredSnapshot = store.getMemTableSnapshot(); + assertEquals("val1_updated", recoveredSnapshot.get("key1")); + assertNull(recoveredSnapshot.get("key2")); + } +} diff --git a/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/LogEntryTest.java b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/LogEntryTest.java new file mode 100644 index 000000000000..ea031e04c8a0 --- /dev/null +++ b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/LogEntryTest.java @@ -0,0 +1,80 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class LogEntryTest { + + @Test + void testToLogStringAndFromLogString() { + LogEntry entry = new LogEntry(1, OperationType.SET, "key1", "val1"); + String logString = entry.toLogString(); + assertEquals("1|SET|key1|val1", logString); + + LogEntry parsed = LogEntry.fromLogString(logString); + assertEquals(entry, parsed); + assertEquals(1, parsed.getSequenceNumber()); + assertEquals(OperationType.SET, parsed.getType()); + assertEquals("key1", parsed.getKey()); + assertEquals("val1", parsed.getValue()); + } + + @Test + void testDeleteLogEntrySerialization() { + LogEntry entry = new LogEntry(2, OperationType.DELETE, "key2", null); + String logString = entry.toLogString(); + assertEquals("2|DELETE|key2|", logString); + + LogEntry parsed = LogEntry.fromLogString(logString); + assertEquals(entry, parsed); + assertEquals("key2", parsed.getKey()); + assertNull(parsed.getValue()); + } + + @Test + void testInvalidLogStringThrowsException() { + assertThrows(IllegalArgumentException.class, () -> LogEntry.fromLogString(null)); + assertThrows(IllegalArgumentException.class, () -> LogEntry.fromLogString(" ")); + assertThrows(IllegalArgumentException.class, () -> LogEntry.fromLogString("1|SET|key1")); + } + + @Test + void testEqualsAndHashCode() { + LogEntry entry1 = new LogEntry(1, OperationType.SET, "k", "v"); + LogEntry entry2 = new LogEntry(1, OperationType.SET, "k", "v"); + LogEntry entry3 = new LogEntry(2, OperationType.SET, "k", "v"); + + assertEquals(entry1, entry2); + assertEquals(entry1.hashCode(), entry2.hashCode()); + assertNotEquals(entry1, entry3); + } +} diff --git a/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/WriteAheadLogTest.java b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/WriteAheadLogTest.java new file mode 100644 index 000000000000..8f0410414e3c --- /dev/null +++ b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/WriteAheadLogTest.java @@ -0,0 +1,134 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class WriteAheadLogTest { + + private File tempFile; + private WriteAheadLog wal; + + @BeforeEach + void setUp() throws IOException { + tempFile = File.createTempFile("wal_test", ".log"); + wal = new WriteAheadLog(tempFile); + } + + @AfterEach + void tearDown() { + if (tempFile != null && tempFile.exists()) { + if (tempFile.isDirectory()) { + File[] files = tempFile.listFiles(); + if (files != null) { + for (File f : files) { + f.delete(); + } + } + } + tempFile.delete(); + } + } + + @Test + void testAppendAndReadAll() throws IOException { + LogEntry e1 = wal.append(OperationType.SET, "k1", "v1"); + LogEntry e2 = wal.append(OperationType.SET, "k2", "v2"); + LogEntry e3 = wal.append(OperationType.DELETE, "k1", null); + + assertEquals(1, e1.getSequenceNumber()); + assertEquals(2, e2.getSequenceNumber()); + assertEquals(3, e3.getSequenceNumber()); + + List entries = wal.readAll(); + assertEquals(3, entries.size()); + assertEquals(e1, entries.get(0)); + assertEquals(e2, entries.get(1)); + assertEquals(e3, entries.get(2)); + } + + @Test + void testSequenceNumberResumptionOnReopen() throws IOException { + wal.append(OperationType.SET, "k1", "v1"); + wal.append(OperationType.SET, "k2", "v2"); + + WriteAheadLog reopenedWal = new WriteAheadLog(tempFile); + LogEntry newEntry = reopenedWal.append(OperationType.SET, "k3", "v3"); + + assertEquals(3, newEntry.getSequenceNumber()); + } + + @Test + void testClear() throws IOException { + wal.append(OperationType.SET, "k1", "v1"); + assertTrue(tempFile.exists()); + + wal.clear(); + assertFalse(tempFile.exists()); + + List entries = wal.readAll(); + assertNotNull(entries); + assertTrue(entries.isEmpty()); + } + + @Test + void testReadAllIOExceptionHandling() throws IOException { + File dir = Files.createTempDirectory("wal_dir_test").toFile(); + WriteAheadLog dirWal = new WriteAheadLog(dir); + + List entries = dirWal.readAll(); + assertNotNull(entries); + assertTrue(entries.isEmpty()); + + dir.delete(); + } + + @Test + void testClearIOExceptionHandling() throws IOException { + File dir = Files.createTempDirectory("wal_nonempty_dir_test").toFile(); + File child = new File(dir, "child.txt"); + child.createNewFile(); + + WriteAheadLog dirWal = new WriteAheadLog(dir); + dirWal.clear(); + + assertTrue(dir.exists()); + + child.delete(); + dir.delete(); + } +} From d7327917cfdced69d9c8707815434333c2678ef0 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 07:48:14 +0300 Subject: [PATCH 5/7] docs: add devikae as a contributor for code (#3586) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 83b0ddaf8072..1d73d46a4e14 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3782,6 +3782,15 @@ "contributions": [ "code" ] + }, + { + "login": "devikae", + "name": "dev-ikae", + "avatar_url": "https://avatars.githubusercontent.com/u/98100047?v=4", + "profile": "https://github.com/devikae", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 6, diff --git a/README.md b/README.md index c5c284f6b29f..8db0c3bfb3a2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=iluwatar_java-design-patterns&metric=coverage)](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns) [![Join the chat at https://gitter.im/iluwatar/java-design-patterns](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-415-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-416-orange.svg?style=flat-square)](#contributors-)
@@ -615,6 +615,7 @@ This project is licensed under the terms of the MIT license. Sandhya
Sandhya

💻 + dev-ikae
dev-ikae

💻 From c8423198cd4b6349c50226dbe9ce7dac9f725dca Mon Sep 17 00:00:00 2001 From: zanarelli Date: Tue, 25 Aug 2026 01:51:26 -0300 Subject: [PATCH 6/7] docs: pt: add localization for polling-publisher, money, delegation patterns (#3583) Add Brazilian Portuguese (pt) translations for three previously untranslated pattern READMEs, following the existing localization/pt/ convention documented in the wiki (15. Support for multiple languages). Frontmatter and section headers follow the same structure used by the most recently updated existing translation (singleton), keeping code blocks, links, and image references unchanged from the English source. --- localization/pt/delegation/README.md | 157 ++++++++++++++++++++ localization/pt/money/README.md | 131 ++++++++++++++++ localization/pt/polling-publisher/README.md | 119 +++++++++++++++ 3 files changed, 407 insertions(+) create mode 100644 localization/pt/delegation/README.md create mode 100644 localization/pt/money/README.md create mode 100644 localization/pt/polling-publisher/README.md diff --git a/localization/pt/delegation/README.md b/localization/pt/delegation/README.md new file mode 100644 index 000000000000..9f424895891d --- /dev/null +++ b/localization/pt/delegation/README.md @@ -0,0 +1,157 @@ +--- +title: "Delegation Pattern in Java: Mastering Efficient Task Assignment" +shortTitle: Delegation +description: "Explore the Delegation Design Pattern in Java with real-world examples, class diagrams, and its benefits. Learn how to enhance your code flexibility and reuse." +category: Behavioral +language: pt +tag: + - Decoupling + - Delegation + - Object composition +--- + +## Também conhecido como + +* Helper +* Surrogate + +## Propósito + +Permitir que um objeto delegue a responsabilidade por uma tarefa a outro objeto auxiliar. + +## Explicação + +Exemplo do mundo real + +> Em um restaurante, o chef principal delega tarefas aos sous-chefs: um cuida das grelhas, outro das saladas e um terceiro é responsável pelas sobremesas. Cada sous-chef é especializado em sua área, permitindo que o chef principal se concentre na gestão geral da cozinha. Isso reflete o padrão Delegation, no qual um objeto principal delega tarefas específicas a objetos auxiliares, cada um especialista em seu domínio. + +Em outras palavras + +> Delegation é um padrão de design em que um objeto repassa uma tarefa a um objeto auxiliar. + +De acordo com a Wikipédia + +> Em programação orientada a objetos, delegação se refere a avaliar um membro (propriedade ou método) de um objeto (o receptor) no contexto de outro objeto original (o remetente). A delegação pode ser feita explicitamente, passando o objeto remetente para o objeto receptor, o que pode ser feito em qualquer linguagem orientada a objetos; ou implicitamente, pelas regras de busca de membros da linguagem, o que exige suporte da linguagem para esse recurso. + +Diagrama de sequência + +![Delegation sequence diagram](../../../delegation/etc/delegation-sequence-diagram.png) + +## Exemplo Programático + +Vamos considerar um exemplo de impressão. + +Temos uma interface `Printer` e três implementações: `CanonPrinter`, `EpsonPrinter` e `HpPrinter`. + +```java +public interface Printer { + void print(final String message); +} + +@Slf4j +public class CanonPrinter implements Printer { + @Override + public void print(String message) { + LOGGER.info("Canon Printer : {}", message); + } +} + +@Slf4j +public class EpsonPrinter implements Printer { + @Override + public void print(String message) { + LOGGER.info("Epson Printer : {}", message); + } +} + +@Slf4j +public class HpPrinter implements Printer { + @Override + public void print(String message) { + LOGGER.info("HP Printer : {}", message); + } +} +``` + +O `PrinterController` pode ser usado como um `Printer`, delegando qualquer trabalho tratado por essa interface a um objeto que a implemente. + +```java +public class PrinterController implements Printer { + + private final Printer printer; + + public PrinterController(Printer printer) { + this.printer = printer; + } + + @Override + public void print(String message) { + printer.print(message); + } +} +``` + +No código cliente, os controladores de impressora podem imprimir mensagens de formas diferentes, dependendo do objeto para o qual delegam esse trabalho. + +```java +public class App { + + private static final String MESSAGE_TO_PRINT = "hello world"; + + public static void main(String[] args) { + var hpPrinterController = new PrinterController(new HpPrinter()); + var canonPrinterController = new PrinterController(new CanonPrinter()); + var epsonPrinterController = new PrinterController(new EpsonPrinter()); + + hpPrinterController.print(MESSAGE_TO_PRINT); + canonPrinterController.print(MESSAGE_TO_PRINT); + epsonPrinterController.print(MESSAGE_TO_PRINT); + } +} +``` + +Saída do programa: + +``` +HP Printer:hello world +Canon Printer:hello world +Epson Printer:hello world +``` + +## Quando usar o padrão Delegation + +* Quando você deseja passar a responsabilidade de uma classe para outra sem usar herança. +* Para obter reutilização baseada em composição em vez de herança. +* Quando você precisa usar diversas classes auxiliares intercambiáveis em tempo de execução. + +## Aplicações do mundo real do padrão Delegation + +* O pacote java.awt.event do Java, no qual listeners são frequentemente usados para tratar eventos. +* Classes wrapper do Java Collections Framework (java.util.Collections), que delegam a outros objetos de coleção. +* No Spring Framework, a delegação é usada extensivamente no container IoC, no qual beans delegam tarefas a outros beans. + +## Benefícios e desafios do padrão Delegation + +Benefícios: + +* Reduz a criação de subclasses: os objetos podem delegar operações a objetos diferentes e alterá-los em tempo de execução, reduzindo a necessidade de criar subclasses. +* Incentiva a reutilização: a delegação promove a reutilização do código do objeto auxiliar. +* Aumenta a flexibilidade: ao delegar tarefas a objetos auxiliares, é possível alterar o comportamento das suas classes em tempo de execução. + +Desafios: + +* Sobrecarga em tempo de execução: a delegação pode introduzir camadas adicionais de indireção, o que pode resultar em pequenos custos de desempenho. +* Complexidade: o design pode se tornar mais complicado, pois envolve classes e interfaces adicionais para gerenciar a delegação. + +## Padrões relacionados + +* [Composite](https://java-design-patterns.com/patterns/composite/): a delegação pode ser usada dentro de um padrão composite para delegar comportamentos específicos de componentes a componentes filhos. +* [Strategy](https://java-design-patterns.com/patterns/strategy/): a delegação é frequentemente usada no padrão strategy, no qual um objeto de contexto delega tarefas a um objeto de estratégia. +* https://java-design-patterns.com/patterns/proxy/: o padrão proxy é uma forma de delegação em que um objeto proxy controla o acesso a outro objeto, ao qual delega o trabalho. + +## Referências e Créditos + +* [Effective Java](https://amzn.to/4aGE7gX) +* [Head First Design Patterns](https://amzn.to/3J9tuaB) +* [Refactoring: Improving the Design of Existing Code](https://amzn.to/3VOcRsw) +* [Delegate Pattern: Wikipedia ](https://en.wikipedia.org/wiki/Delegation_pattern) diff --git a/localization/pt/money/README.md b/localization/pt/money/README.md new file mode 100644 index 000000000000..278924fb1315 --- /dev/null +++ b/localization/pt/money/README.md @@ -0,0 +1,131 @@ +--- +title: "Money Pattern in Java: Encapsulating Monetary Values with Currency Consistency" +shortTitle: Money +description: "Learn how the Money design pattern in Java ensures currency safety, precision handling, and maintainable financial operations. Explore examples, applicability, and benefits of the pattern." +category: Structural +language: pt +tag: + - Business + - Domain + - Encapsulation + - Immutable +--- + +## Também conhecido como + +* Monetary Value Object + +## Propósito + +Encapsular valores monetários e sua moeda associada em um objeto específico do domínio. + +## Explicação + +Exemplo do mundo real + +> Imagine um sistema de vale-presente on-line, no qual cada vale-presente mantém um saldo específico em uma determinada moeda. Em vez de usar apenas um valor de ponto flutuante para o saldo, o sistema usa um objeto Money para rastrear o valor e a moeda com precisão. Sempre que alguém usa o vale-presente, o saldo é atualizado com cálculos precisos que evitam erros de arredondamento de ponto flutuante, garantindo que a lógica de domínio permaneça consistente e correta. + +Em outras palavras + +> O padrão Money encapsula tanto um valor quanto sua moeda, garantindo que as operações financeiras sejam precisas, consistentes e fáceis de manter. + +De acordo com a Wikipédia + +> O padrão de design Money encapsula um valor monetário e sua moeda, permitindo operações aritméticas e conversões seguras, ao mesmo tempo em que preserva a precisão e a consistência nos cálculos financeiros. + +Mapa mental + +![Money Pattern Mind Map](../../../money/etc/money-mind-map.png) + +Fluxograma + +![Money Pattern Flowchart](../../../money/etc/money-flowchart.png) + +## Exemplo Programático + +Neste exemplo, criamos uma classe `Money` para demonstrar como valores monetários podem ser encapsulados junto com sua moeda. Essa abordagem ajuda a evitar imprecisões de ponto flutuante, garante que as operações aritméticas sejam tratadas de forma consistente e fornece uma maneira clara e centrada no domínio de trabalhar com dinheiro. + +```java +@AllArgsConstructor +@Getter +public class Money { + private double amount; + private String currency; + + public Money(double amnt, String curr) { + this.amount = amnt; + this.currency = curr; + } + + private double roundToTwoDecimals(double value) { + return Math.round(value * 100.0) / 100.0; + } + + public void addMoney(Money moneyToBeAdded) throws CannotAddTwoCurrienciesException { + if (!moneyToBeAdded.getCurrency().equals(this.currency)) { + throw new CannotAddTwoCurrienciesException("You are trying to add two different currencies"); + } + this.amount = roundToTwoDecimals(this.amount + moneyToBeAdded.getAmount()); + } + + public void subtractMoney(Money moneyToBeSubtracted) throws CannotSubtractException { + if (!moneyToBeSubtracted.getCurrency().equals(this.currency)) { + throw new CannotSubtractException("You are trying to subtract two different currencies"); + } else if (moneyToBeSubtracted.getAmount() > this.amount) { + throw new CannotSubtractException("The amount you are trying to subtract is larger than the amount you have"); + } + this.amount = roundToTwoDecimals(this.amount - moneyToBeSubtracted.getAmount()); + } + + public void multiply(int factor) { + if (factor < 0) { + throw new IllegalArgumentException("Factor must be non-negative"); + } + this.amount = roundToTwoDecimals(this.amount * factor); + } + + public void exchangeCurrency(String currencyToChangeTo, double exchangeRate) { + if (exchangeRate < 0) { + throw new IllegalArgumentException("Exchange rate must be non-negative"); + } + this.amount = roundToTwoDecimals(this.amount * exchangeRate); + this.currency = currencyToChangeTo; + } +} +``` + +Ao encapsular toda a lógica relacionada a dinheiro em uma única classe, reduzimos o risco de misturar moedas diferentes, melhoramos a clareza do código-base e facilitamos futuras modificações, como adicionar novas moedas ou refinar as regras de arredondamento. Esse padrão fortalece o modelo de domínio ao tratar o dinheiro como um conceito distinto, e não apenas como mais um valor numérico. + +## Quando usar o padrão Money + +* Quando cálculos financeiros ou manipulações de dinheiro fazem parte da lógica de negócio +* Quando é necessário um tratamento preciso de valores monetários para evitar imprecisões de ponto flutuante +* Quando princípios de domain-driven design e tipagem forte são desejados + +## Aplicações do mundo real do padrão Money + +* A biblioteca JSR 354 (Java Money and Currency) em Java +* Modelos de domínio personalizados em sistemas de e-commerce e contabilidade + +## Benefícios e desafios do padrão Money + +Benefícios + +* Fornece uma representação única e type-safe para valores monetários e moeda +* Incentiva o encapsulamento de operações relacionadas, como adição, subtração e formatação +* Evita erros de ponto flutuante ao usar inteiros ou bibliotecas decimais especializadas + +Desafios + +* Requer classes e infraestrutura adicionais para lidar com conversões e formatação de moeda +* Pode introduzir sobrecarga de desempenho ao realizar um grande número de operações monetárias + +## Padrões relacionados + +* [Value Object](https://java-design-patterns.com/patterns/value-object/): Money é tipicamente um exemplo clássico de value object em domain-driven design. + +## Referências e Créditos + +* [Domain-Driven Design: Tackling Complexity in the Heart of Software](https://amzn.to/3wlDrze) +* [Implementing Domain-Driven Design](https://amzn.to/4dmBjrB) +* [Patterns of Enterprise Application Architecture](https://amzn.to/3WfKBPR) diff --git a/localization/pt/polling-publisher/README.md b/localization/pt/polling-publisher/README.md new file mode 100644 index 000000000000..d9cf98a550b3 --- /dev/null +++ b/localization/pt/polling-publisher/README.md @@ -0,0 +1,119 @@ +--- +title: "Polling Publisher-Subscriber Pattern in Java: Mastering Asynchronous Messaging Elegantly" +shortTitle: Polling Pub/Sub +description: "Learn how to implement a Polling Publisher-Subscriber system in Java using Spring Boot and Kafka. Explore the architecture, real-world analogies, and benefits of asynchronous communication with clean code examples." +category: Architectural +language: pt +tag: + - Spring Boot + - Kafka + - Microservices + - Asynchronous Messaging + - Decoupling +--- + +## Também conhecido como + +* Event-Driven Architecture +* Asynchronous Pub/Sub Pattern +* Message Queue-Based Polling System + +## Propósito + +O padrão Polling Publisher-Subscriber desacopla os produtores de dados dos consumidores ao permitir uma comunicação assíncrona e orientada a mensagens. Um serviço consulta periodicamente (poll) uma fonte de dados e publica mensagens em um message broker (por exemplo, Kafka), que são então consumidas por um ou mais serviços assinantes. + +## Explicação + +### Exemplo do mundo real + +> Uma agência de notícias consulta constantemente as últimas atualizações. Assim que recebe novas informações, ela as publica em diferentes veículos (TV, jornais, aplicativos). Cada veículo consome e exibe as atualizações de forma independente. + +### Em outras palavras + +> Um serviço verifica regularmente se há atualizações (polling) e envia mensagens para o Kafka. Outro serviço escuta o Kafka e processa as mensagens de forma assíncrona. + +### De acordo com a Wikipédia + +> Este padrão se assemelha muito ao [modelo Publish–subscribe](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern), no qual as mensagens são enviadas pelos publicadores e recebidas pelos assinantes sem que eles se conheçam. + +### Fluxo da arquitetura + +``` ++------------+ +--------+ +-------------+ +| Publisher | ---> | Kafka | ---> | Subscriber | ++------------+ +--------+ +-------------+ +``` + +## Exemplo Programático (Spring Boot + Kafka) + +### Serviço Publisher + +- Usa o `@Scheduled` do Spring para consultar dados periodicamente. +- Publica dados em um tópico do Kafka. +- Opcionalmente expõe uma API REST para publicação manual de dados. + +```java +@Scheduled(fixedRate = 5000) +public void pollAndPublish() { + String data = pollingService.getLatestData(); + kafkaTemplate.send("updates-topic", data); +} +``` + +### Serviço Subscriber + +- Escuta um tópico do Kafka usando `@KafkaListener`. +- Processa as mensagens de forma assíncrona. + +```java +@KafkaListener(topics = "updates-topic") +public void processUpdate(String message) { + log.info("Received update: {}", message); + updateProcessor.handle(message); +} +``` + +## Quando usar o padrão Polling Publisher-Subscriber + +Use esse padrão quando: + +* Não é possível o produtor enviar dados em tempo real (push). +* Deseja-se um baixo acoplamento entre produtores e consumidores. +* É necessário processamento de eventos assíncrono e escalável. +* Você está construindo uma arquitetura de microsserviços orientada a eventos. + +## Aplicações do mundo real + +* Painéis de relatórios em tempo real +* Agregadores de health check para sistemas distribuídos +* Processamento de telemetria de IoT +* Sistemas de notificação e alerta + +## Benefícios e desafios do padrão Polling Pub/Sub + +### Benefícios + +* Baixo acoplamento entre serviços +* Arquitetura assíncrona e escalável +* Tolerante a falhas, com persistência de mensagens no Kafka +* Fácil de estender com novos consumidores ou publicadores + +### Desafios + +* O polling introduz latência entre a geração e o consumo dos dados +* Requer o gerenciamento e a configuração do Kafka (ou de outro broker) +* Implantação e infraestrutura ligeiramente mais complexas + +## Padrões relacionados + +* [Observer Pattern](https://java-design-patterns.com/patterns/observer/) +* [Mediator Pattern](https://java-design-patterns.com/patterns/mediator/) +* [Message Queue Pattern](https://java-design-patterns.com/patterns/event-queue/) + +## Referências e Créditos + +* [Apache Kafka Documentation](https://kafka.apache.org/documentation/) +* [Spring Kafka Documentation](https://docs.spring.io/spring-kafka) +* [Spring Scheduled Tasks](https://www.baeldung.com/spring-scheduled-tasks) +* [Spring Kafka Tutorial – Baeldung](https://www.baeldung.com/spring-kafka) +* Inspired by: [iluwatar/java-design-patterns](https://github.com/iluwatar/java-design-patterns) From 126ad5fa1f064676ee6dde1a220db2a766375c23 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 07:52:05 +0300 Subject: [PATCH 7/7] docs: add zanarellidev as a contributor for translation (#3587) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1d73d46a4e14..ccc740df15e1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3791,6 +3791,15 @@ "contributions": [ "code" ] + }, + { + "login": "zanarellidev", + "name": "zanarelli", + "avatar_url": "https://avatars.githubusercontent.com/u/268068569?v=4", + "profile": "https://github.com/zanarellidev", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 6, diff --git a/README.md b/README.md index 8db0c3bfb3a2..eb821177e73e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=iluwatar_java-design-patterns&metric=coverage)](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns) [![Join the chat at https://gitter.im/iluwatar/java-design-patterns](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-416-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-417-orange.svg?style=flat-square)](#contributors-)
@@ -616,6 +616,7 @@ This project is licensed under the terms of the MIT license. Sandhya
Sandhya

💻 dev-ikae
dev-ikae

💻 + zanarelli
zanarelli

🌍