How to Implement a Custom Raft Log Storage Solution Using Dragonboat's LogDB Interface

You can implement a custom Raft log storage solution in Dragonboat by implementing the ILogDB interface defined in the raftio package, creating a factory that satisfies the LogDBFactory interface, and injecting it into NodeHostConfig.Expert.LogDBFactory before starting your NodeHost.

Dragonboat is a high-performance Go implementation of the Raft consensus protocol. While it ships with a built-in Pebble-based storage backend, production environments often require specialized storage solutions—whether for compliance, performance optimization, or integration with existing infrastructure. By leveraging the LogDB abstraction, you can replace the default storage engine with any custom backend, from in-memory caches to distributed databases or cloud object storage.

Understanding the LogDB Architecture

The ILogDB Interface Contract

The core contract for custom storage implementations is the ILogDB interface, defined in raftio/logdb.go (lines 59-110). This interface mandates methods for persisting Raft state, managing log entries, and handling snapshots. Key methods include:

  • SaveRaftState(updates []pb.Update, shardID uint64) error – Persists Raft state and log entries
  • IterateEntries(ents []pb.Entry, size uint64, shardID, replicaID, low, high, maxSize uint64) – Retrieves log entries within a specific range
  • ReadRaftState(shardID, replicaID, lastIdx uint64) – Reads the current persistent state
  • CompactEntriesTo(shardID, replicaID, idx uint64) – Removes entries up to a specific index
  • SaveSnapshots and GetSnapshot – Handle snapshot persistence

The LogDBFactory Extension Point

Dragonboat instantiates storage through a factory pattern defined in config/config.go (lines 487-495). The LogDBFactory interface requires:

  • Create(cfg NodeHostConfig, cb LogDBCallback, dirs []string, wals []string) (raftio.ILogDB, error)
  • Name() string

If you do not provide a custom factory, Dragonboat defaults to the built-in Pebble implementation (previously RocksDB) via defaultLogDB in the Prepare method (lines 83-86 of config/config.go).

Step-by-Step Implementation Guide

Step 1: Implement the ILogDB Interface

Create a struct that satisfies all methods of raftio.ILogDB. The implementation must be thread-safe, as Dragonboat may invoke these methods concurrently. Here is a minimal in-memory skeleton:

type customLogDB struct {
    mu    sync.RWMutex
    state map[raftio.NodeInfo]raftio.RaftState
    logs  map[raftio.NodeInfo][]pb.Entry
    snaps map[raftio.NodeInfo]pb.Snapshot
}

func (c *customLogDB) Name() string { return "custom" }
func (c *customLogDB) Close() error { return nil }
func (c *customLogDB) BinaryFormat() uint32 { return raftio.PlainLogDBBinVersion }

// Implement remaining methods: SaveRaftState, IterateEntries, ReadRaftState, etc.

Step 2: Create the LogDBFactory

Implement the factory that Dragonboat will call during NodeHost initialization:

type customFactory struct{}

func (f customFactory) Create(cfg config.NodeHostConfig, cb config.LogDBCallback, dirs []string, wals []string) (raftio.ILogDB, error) {
    return &customLogDB{
        state: make(map[raftio.NodeInfo]raftio.RaftState),
        logs:  make(map[raftio.NodeInfo][]pb.Entry),
        snaps: make(map[raftio.NodeInfo]pb.Snapshot),
    }, nil
}

func (f customFactory) Name() string { return "custom" }

Step 3: Configure NodeHost to Use Your Factory

Wire the factory into your NodeHost configuration before starting:

cfg := config.NodeHostConfig{
    DeploymentID: 1,
    WALDir:       "./wal",
    NodeHostDir:  "./data",
}

// Attach the custom LogDB factory
cfg.Expert.LogDBFactory = customFactory{}

if err := cfg.Prepare(); err != nil {
    log.Fatalf("config error: %v", err)
}

nh, err := dragonboat.NewNodeHost(cfg, config.DefaultRAFTAddress)
if err != nil {
    log.Fatalf("failed to start NodeHost: %v", err)
}

Note: While the deprecated cfg.LogDBFactory field still works, the modern approach uses cfg.Expert.LogDBFactory as implemented in config/config.go.

Complete Working Example: In-Memory LogDB

Below is a complete, runnable in-memory implementation demonstrating all required methods. This example is suitable for testing or ephemeral clusters where durability is not required.

package main

import (
    "sync"
    
    "github.com/lni/dragonboat/v4/config"
    "github.com/lni/dragonboat/v4/raftio"
    "github.com/lni/dragonboat/v4/raftpb"
)

type memLogDB struct {
    mu    sync.RWMutex
    state map[raftio.NodeInfo]raftio.RaftState
    logs  map[raftio.NodeInfo][]raftpb.Entry
    snaps map[raftio.NodeInfo]raftpb.Snapshot
}

func (m *memLogDB) Name() string                         { return "memlogdb" }
func (m *memLogDB) Close() error                         { return nil }
func (m *memLogDB) BinaryFormat() uint32                 { return raftio.PlainLogDBBinVersion }
func (m *memLogDB) ListNodeInfo() ([]raftio.NodeInfo, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    var res []raftio.NodeInfo
    for ni := range m.state {
        res = append(res, ni)
    }
    return res, nil
}
func (m *memLogDB) SaveBootstrapInfo(shardID, replicaID uint64, b raftpb.Bootstrap) error {
    return nil
}
func (m *memLogDB) GetBootstrapInfo(shardID, replicaID uint64) (raftpb.Bootstrap, error) {
    return raftpb.Bootstrap{}, raftio.ErrNoBootstrapInfo
}
func (m *memLogDB) SaveRaftState(updates []raftpb.Update, shardID uint64) error {
    m.mu.Lock()
    defer m.mu.Unlock()
    for _, ud := range updates {
        ni := raftio.GetNodeInfo(ud.ShardID, ud.ReplicaID)
        m.logs[ni] = append(m.logs[ni], ud.Entries...)
        if ud.State != nil {
            m.state[ni] = raftio.RaftState{
                State:      *ud.State,
                FirstIndex: ud.FirstIndex,
                EntryCount: uint64(len(m.logs[ni])),
            }
        }
    }
    return nil
}
func (m *memLogDB) IterateEntries(ents []raftpb.Entry, size uint64,
    shardID, replicaID, low, high, maxSize uint64) ([]raftpb.Entry, uint64, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    ni := raftio.GetNodeInfo(shardID, replicaID)
    log := m.logs[ni]
    if low >= uint64(len(log)) {
        return nil, 0, nil
    }
    var total uint64
    var out []raftpb.Entry
    for i := low; i < high && i < uint64(len(log)); i++ {
        e := log[i]
        if total+uint64(e.Size()) > maxSize {
            break
        }
        out = append(out, e)
        total += uint64(e.Size())
    }
    return out, total, nil
}
func (m *memLogDB) ReadRaftState(shardID, replicaID, lastIdx uint64) (raftio.RaftState, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    return m.state[raftio.GetNodeInfo(shardID, replicaID)], nil
}
func (m *memLogDB) RemoveEntriesTo(shardID, replicaID, idx uint64) error { return nil }
func (m *memLogDB) CompactEntriesTo(shardID, replicaID, idx uint64) (<-chan struct{}, error) {
    ch := make(chan struct{}, 1)
    ch <- struct{}{}
    return ch, nil
}
func (m *memLogDB) SaveSnapshots(updates []raftpb.Update) error { return nil }
func (m *memLogDB) GetSnapshot(shardID, replicaID uint64) (raftpb.Snapshot, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    return m.snaps[raftio.GetNodeInfo(shardID, replicaID)], nil
}
func (m *memLogDB) RemoveNodeData(shardID, replicaID uint64) error { return nil }
func (m *memLogDB) ImportSnapshot(snap raftpb.Snapshot, replicaID uint64) error { return nil }

Key Source Files and Reference Implementations

When building a production-grade custom storage backend, study these files in the lni/dragonboat repository:

  • raftio/logdb.go – Defines the ILogDB interface and related types (NodeInfo, RaftState, ErrNoBootstrapInfo). This is the primary contract your implementation must satisfy.
  • config/config.go – Contains the LogDBFactory interface definition (lines 487-495) and the NodeHostConfig.Expert.LogDBFactory field used to inject your factory.
  • internal/logdb/logdb.go – Reference implementation of the built-in Pebble-backed LogDB. Study this for patterns on batching, compaction, and error handling.
  • plugin/tee/tee.go – Example of a factory wrapper (TanPebbleLogDBFactory) that demonstrates how to compose multiple storage backends.
  • docs/storage.md – High-level documentation summarizing the custom storage integration process.

Summary

Implementing a custom Raft log storage solution using Dragonboat's LogDB interface requires three main steps:

  • Implement ILogDB – Create a thread-safe struct satisfying all methods in raftio/logdb.go, including SaveRaftState, IterateEntries, and ReadRaftState.
  • Create a Factory – Build a LogDBFactory implementation that instantiates your custom LogDB, providing the Create and Name methods as defined in config/config.go.
  • Configure NodeHost – Assign your factory to NodeHostConfig.Expert.LogDBFactory (or the deprecated LogDBFactory field) before calling NewNodeHost.

By following this pattern, you can integrate any storage backend—from in-memory caches to distributed databases—while Dragonboat handles the Raft consensus logic.

Frequently Asked Questions

What methods are mandatory when implementing the ILogDB interface?

You must implement all methods defined in raftio/logdb.go, including SaveRaftState for persisting Raft updates, IterateEntries for log retrieval, ReadRaftState for reading persistent state, CompactEntriesTo for log compaction, and SaveSnapshots/GetSnapshot for snapshot management. The interface also requires ListNodeInfo, SaveBootstrapInfo, and lifecycle methods like Close and Name.

Can I use multiple different storage backends simultaneously in one NodeHost?

Yes, you can implement a composite factory that routes different shards to different backends, or use a wrapper pattern like the TanPebbleLogDBFactory shown in plugin/tee/tee.go. However, each individual shard must use a consistent storage backend throughout its lifetime to maintain Raft safety guarantees.

Is the in-memory implementation shown in the examples suitable for production use?

No, the in-memory memLogDB example is intended for testing and demonstration only. Production implementations must provide durable persistence, proper crash recovery, and efficient compaction. Study the reference implementation in internal/logdb/logdb.go for production-grade patterns using Pebble.

What is the difference between LogDBFactory and Expert.LogDBFactory?

LogDBFactory is the deprecated field in NodeHostConfig for specifying custom storage factories. The modern approach uses Expert.LogDBFactory (defined in config/config.go), which provides better encapsulation of advanced configuration options. Both currently work, but Expert.LogDBFactory is the recommended path for new implementations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →