Adetayo Akinsanya unkletayo.dev
Engineering / Kafka from First Principles • Part 3 of 20 Published

The Append-Only Log Abstraction: Why Immutability Rules Event Streaming

Monotonically increasing offsets, lock-free concurrency, and multi-consumer replay.

Part 3 in Series — Catch up on the previous article: Kafka Performance Secrets: Why Sequential Disk I/O Beats Random RAM Access (Part 2) before diving into this post.

Suppose you are auditing a traditional financial bank ledger.

An accountant records a deposit of $500 at 9:00 AM. At 10:00 AM, the customer withdraws $200.

How does the bank ledger record this change? Does the accountant erase the original $500 entry with an eraser and overwrite the paper page with $300?

No. Erasing historical paper records is illegal in financial accounting.

Instead, the accountant appends a new entry to the end of the journal: Withdrawal: -$200. Current account balance is computed by reading the ordered sequence of historical transactions from the start of the log to the end.

This fundamental data structure is the Append-Only Log.


Why You Need This in Real Life

In software engineering, developers often confuse a log file (app.log text lines) with the Log Data Structure.

The Log as an abstract data structure is an ordered, immutable, append-only sequence of records.

THE APPEND-ONLY LOG ABSTRACTION

Offset:      0        1        2        3        4        5
        +--------+--------+--------+--------+--------+--------+
        | Record | Record | Record | Record | Record | Record | ---> Append New Records
        +--------+--------+--------+--------+--------+--------+

Every incoming record receives a monotonically increasing 64-bit integer identifier called an Offset.

Once a record is written to a log, it becomes immutable. It cannot be edited, overwritten, or re-ordered.


The Power of Immutability in Distributed Systems

Mutating state in place is the source of concurrency bugs in software development.

Consider what happens when multiple services access a mutable database row:

MUTABLE DATABASE STATE (Row ID 42)
Thread A reads row 42 -> Thread B overwrites row 42 -> Thread A writes stale data!
(Requires heavy lock synchronization across threads)

When data is stored inside an immutable append-only log, lock contention vanishes:

  1. Zero Lock Concurrency: Readers can scan past log records at high speed while a writer appends new records to the tail of the file. Readers and writers do not block each other.
  2. Simplified Replication: Replicating state across servers reduces to copying log bytes sequentially from offset NN to offset N+KN+K.
  3. Deterministic Replay: If an application service crashes mid-processing, it can recover its exact state by replaying records from offset 0 up to its last committed checkpoint offset.

Multiple Independent Consumers with Custom Pointers

In a traditional message queue (e.g. JMS, RabbitMQ), the broker tracks which messages have been consumed and deletes them from memory.

In an append-only log, the log remains untouched by readers.

Instead of the broker tracking read state, every consumer maintains its own independent offset read pointer.

LOG OFFSET DATA
Offset:    0      1      2      3      4      5      6      7
        +------+------+------+------+------+------+------+------+
        | Msg  | Msg  | Msg  | Msg  | Msg  | Msg  | Msg  | Msg  |
        +------+------+------+------+------+------+------+------+
                   ^                    ^                    ^
                   |                    |                    |
        Consumer Group A Offset: 1   Consumer Group B Offset: 4   Producer Write Tail: 7

This simple design delivers massive operational flexibility:

  • Independent Reading Speeds: Consumer A can process real-time events at offset 7, while Consumer B (a batch analytics job) slowly processes historical events at offset 1.
  • Zero Reader Interference: Adding 50 new consumer services to read the log adds zero write lock contention.
  • Reprocessing & Bug Recovery: If a bug corrupts downstream data processing on Tuesday, developers can reset Consumer Group A’s read offset pointer back to Monday’s offset and replay the entire stream.

Building a Toy Append-Only Log in Java

To understand how an append-only log functions on disk, let’s build a minimal ToyLogManager that writes fixed-length records and reads entries by offset index:

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class ToyLogManager {
    private static final int RECORD_HEADER_SIZE = 12; // 8 bytes offset + 4 bytes length
    private final FileChannel logChannel;
    private long currentOffset = 0;

    public ToyLogManager(File logFile) throws Exception {
        @SuppressWarnings("resource")
        RandomAccessFile raf = new RandomAccessFile(logFile, "rw");
        this.logChannel = raf.getChannel();
        this.currentOffset = calculateOffsetCount();
    }

    public synchronized long append(byte[] payload) throws Exception {
        long assignedOffset = currentOffset;

        ByteBuffer buffer = ByteBuffer.allocate(RECORD_HEADER_SIZE + payload.length);
        buffer.putLong(assignedOffset);       // 8 bytes: Offset
        buffer.putInt(payload.length);        // 4 bytes: Payload Length
        buffer.put(payload);                 // Payload bytes
        buffer.flip();

        logChannel.write(buffer, logChannel.size()); // Append to physical end of file
        currentOffset++;
        return assignedOffset;
    }

    public byte[] readRecord(long targetOffset) throws Exception {
        logChannel.position(0);
        ByteBuffer headerBuffer = ByteBuffer.allocate(RECORD_HEADER_SIZE);

        while (logChannel.position() < logChannel.size()) {
            headerBuffer.clear();
            int read = logChannel.read(headerBuffer);
            if (read < RECORD_HEADER_SIZE) break;

            headerBuffer.flip();
            long offset = headerBuffer.getLong();
            int length = headerBuffer.getInt();

            if (offset == targetOffset) {
                ByteBuffer payloadBuffer = ByteBuffer.allocate(length);
                logChannel.read(payloadBuffer);
                return payloadBuffer.array();
            } else {
                // Skip payload bytes to reach next record header
                logChannel.position(logChannel.position() + length);
            }
        }
        return null; // Offset not found
    }

    private long calculateOffsetCount() throws Exception {
        // Counts existing records in log file on startup
        long count = 0;
        logChannel.position(0);
        ByteBuffer headerBuffer = ByteBuffer.allocate(RECORD_HEADER_SIZE);
        while (logChannel.position() < logChannel.size()) {
            headerBuffer.clear();
            if (logChannel.read(headerBuffer) < RECORD_HEADER_SIZE) break;
            headerBuffer.flip();
            headerBuffer.getLong();
            int length = headerBuffer.getInt();
            logChannel.position(logChannel.position() + length);
            count++;
        }
        return count;
    }
}

This toy implementation highlights two core log characteristics:

  1. Records append to the end of the channel file without seeking or mutating existing bytes.
  2. Every record has a permanent offset position.

In production Kafka, scanning records from the start of a file is optimized using sparse index files, which we will build in Part 06.


Quick Summary

  • An append-only log is an ordered, immutable sequence of records where each record receives a sequential 64-bit offset.
  • Immutability eliminates lock contention between readers and writers, simplifying multi-node replication.
  • Consumers maintain their own independent offset read pointers, enabling multi-team data sharing, independent reading speeds, and historical reprocessing.

References & Further Reading

  1. Apache Software Foundation. Apache Kafka Documentation: Log Anatomy & Segment Configuration. Apache Kafka Docs.
  2. Shapira, G., et al. (2021). Kafka: The Definitive Guide (2nd Edition) — Chapter 5: Reliable Data Delivery. O’Reilly Media.
  3. Kreps, J. (2013). The Log. LinkedIn Engineering Blog.

Up Next in Series →

Part 4: Kafka Architecture Deep Dive: Topics, Partitions, and Offset Ordering Rules

Continue to Part 4 →