-
-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat: Implement Write-Ahead Log (WAL) pattern (#3576) #3582
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
iluwatar
merged 2 commits into
iluwatar:master
from
devikae:feat/write-ahead-log-pattern
Aug 25, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
| <<enumeration>> | ||
| 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<String, String> 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<LogEntry> 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/) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!-- | ||
|
|
||
| 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. | ||
|
|
||
| --> | ||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| <modelVersion>4.0.0</modelVersion> | ||
| <parent> | ||
| <groupId>com.iluwatar</groupId> | ||
| <artifactId>java-design-patterns</artifactId> | ||
| <version>1.26.0-SNAPSHOT</version> | ||
| </parent> | ||
| <artifactId>write-ahead-log</artifactId> | ||
| <version>1.26.0-SNAPSHOT</version> | ||
| <name>write-ahead-log</name> | ||
| <url>http://maven.apache.org</url> | ||
| <properties> | ||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
| </properties> | ||
| <dependencies> | ||
| <dependency> | ||
| <groupId>org.slf4j</groupId> | ||
| <artifactId>slf4j-api</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>ch.qos.logback</groupId> | ||
| <artifactId>logback-classic</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.projectlombok</groupId> | ||
| <artifactId>lombok</artifactId> | ||
| <version>${lombok.version}</version> | ||
| <scope>provided</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.junit.jupiter</groupId> | ||
| <artifactId>junit-jupiter-engine</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.mockito</groupId> | ||
| <artifactId>mockito-core</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| </dependencies> | ||
| <build> | ||
| <plugins> | ||
| <plugin> | ||
| <groupId>org.apache.maven.plugins</groupId> | ||
| <artifactId>maven-assembly-plugin</artifactId> | ||
| <executions> | ||
| <execution> | ||
| <configuration> | ||
| <archive> | ||
| <manifest> | ||
| <mainClass>com.iluwatar.writeaheadlog.App</mainClass> | ||
| </manifest> | ||
| </archive> | ||
| </configuration> | ||
| </execution> | ||
| </executions> | ||
| </plugin> | ||
| </plugins> | ||
| </build> | ||
| </project> |
82 changes: 82 additions & 0 deletions
82
write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/App.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| * | ||
| * <p>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); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.