PRODUCTS

KEYWORDS

DumboDB: Announcing Replication V1

DumboDB Logo

DumboDB is what you get when MongoDB and Git have a baby. Every document in your database now has a revision history, similar to how you track versions of your source code. It’s Git for your documents. The Merkle DAG of Git allows for branching and merging, while document-based applications continue to use the familiar MongoDB ecosystem to query and update data.

DoltHub’s first and most mature product is Dolt, which is what happens when MySQL and Git have a baby. One of the ways in which Dolt participates in the MySQL ecosystem is by supporting binlog replication, which enables MySQL operators to see fine-grained operations on their data, with each source transaction recorded as a Dolt commit.

The use case translates very easily to MongoDB. MongoDB supports a replication system that has been battle-tested for a long time. MongoDB replica sets are used to move data between hosts for redundancy and resiliency. Until today, DumboDB had no ability to participate in a MongoDB replica set.

Today we announce the ability for DumboDB to participate in a MongoDB replica set as a change sink. This allows MongoDB operators and testers the ability to get a fine-grained log of changes in their database in the form of the DumboDB commit log. Let’s dive in!

MongoDB Replication#

MongoDB replication is one of the reasons people think of MongoDB as “Web Scale.”

Jokes aside, for MongoDB, it’s fairly trivial to add more read capacity and increase availability with fail over options using replica sets. This is all built into the server and doesn’t depend on Kubernetes or containers.

At a high level, there are peers in the network, some of which have “voting” ability. These votes are used to assure quorum when doing primary election and so forth.

If you’re wondering whether that’s gossip or something more formal, it’s the latter: MongoDB’s replication is closer to Raft than to a gossip protocol, though it isn’t Raft exactly. Every member heartbeats every other member, and when the primary stops answering, the remaining voters hold an election. The winner earns a term, a number that only ever goes up and gets stamped on every write it accepts afterwards. Terms are what make a zombie primary harmless: anything it wrote carries an older term, so instead of being accepted, it gets rolled back.

One interesting divergence from Raft is that the log flows the other way. Rather than the primary pushing entries out to its followers, each secondary tails the primary’s local.oplog.rs collection and pulls what it needs. That’s also why a secondary can sync from another secondary rather than always from the primary. There is a good deal more under the hood here, and MongoDB’s replication documentation is the place to go for the full picture.

DumboDB as a Replica#

Each host that participates in the replica set can join at varying levels of participation. DumboDB exploits this fact by only allowing itself to participate as a non-voting, hidden member with a priority of zero, which means it can never be elected primary, never counts toward the quorum that acknowledges a write, and never shows up to clients discovering the set. It reads the oplog like any other secondary, and is otherwise invisible to everyone.

Participating as a full member of a MongoDB replica set or enabling full replica support in DumboDB isn’t currently planned, but we love hearing from our users if that’s something that would interest you!

No Authentication, Yet#

One major limitation DumboDB has currently is that replication is not supported in an environment where --auth is enabled. This also includes TLS transports, since these go hand in hand. Support will be added in future releases!

Example#

We’ll set up a MongoDB and DumboDB instance to demonstrate how updates in your MongoDB server can be read in the commit log of DumboDB. Setup is a little involved, mainly because you need both a MongoDB instance and a DumboDB instance running to see the magic. We’ll take it slow!

Run an 8.0.x Version of MongoDB#

If you don’t already have one, grab the MongoDB Community Server from the download page, or follow the installation guide for your platform. You’ll want the mongosh shell too, which is a separate download.

Everything below was run against mongod 8.0.28. Start it as a single-member replica set. Open a terminal and let this one run:

mkdir -p /tmp/demo/mongo
mongod --replSet rs0 --port 27017 --dbpath /tmp/demo/mongo \
       --bind_ip 127.0.0.1 --nounixsocket

Connect to the MongoDB instance with mongosh, on the default port:

$ mongosh mongodb://localhost

Then initiate the set with MongoDB alone in it. DumboDB joins in a moment.

rs0 [direct: other] test> rs.initiate({ 
         _id: "rs0",
         members: [ { _id: 0, host: "127.0.0.1:27017" } ] 
})
{ ok: 1 }

Since there are no other members of the replica set, this host will become the primary quickly. You can check it with:

rs0 [direct: primary] test> rs.status().myState
1                                              // Primary

It is worth calling out the shell prompt, rs0 [direct: primary] test>, and how it changed from other to primary. That prompt appears in the examples below and indicates we are connected to MongoDB (primary) or DumboDB (secondary).

Run a 0.6.4 Instance of DumboDB#

Grab the v0.6.4 release. Any earlier version will not support this demo! We publish pre-built binaries for Linux, MacOS and Windows on the releases page; drop the one for your platform somewhere on your PATH. There’s a Docker image and build-from-source instructions in the README if you prefer either of those.

The --replSet flag is what puts DumboDB in replica mode. Without it you get an ordinary standalone DumboDB server.

Open a second terminal for this one and leave it running too:

mkdir -p /tmp/demo/dumbo
dumbodb --replSet rs0 --addr 127.0.0.1:27018 --data-dir /tmp/demo/dumbo

Join the Replica Set#

DumboDB joins as a hidden, non-voting, priority-zero member. All three matter, and DumboDB refuses to replicate without them. Using the mongosh shell connected to the primary server:

rs0 [direct: primary] test> cfg = rs.conf()
rs0 [direct: primary] test> cfg.version = cfg.version + 1
rs0 [direct: primary] test> cfg.members.push( {
     _id: 1,                        // must be unique within the set
     host: "127.0.0.1:27018",
     hidden: true,                  // invisible to client discovery
     priority: 0,                   // never eligible to become primary
     votes: 0                       // never counted for quorum
})
rs0 [direct: primary] test> rs.reconfig(cfg)
{ ok: 1 }

You can see that the mechanism for this configuration uses a compare and set on the version number of the original configuration. We read the existing configuration, then push the details for the DumboDB instance - keyed by its _id.

Careful: Both _id and host have to be unique within the set, but MongoDB enforces them differently. A duplicate _id, or a host that exactly matches another member’s, is rejected outright. The host check is a string comparison though, so be careful to not mix up equivalent endpoints, like localhost:27018 and 127.0.0.1:27018.

After a few seconds, both members report in:

rs0 [direct: primary] test> rs.status().members.map(m => [m.name, m.stateStr])
[
  [ '127.0.0.1:27017', 'PRIMARY' ],
  [ '127.0.0.1:27018', 'SECONDARY' ]
]

Write to MongoDB, Read from DumboDB#

All writes happen on the MongoDB primary. DumboDB is never written to directly; in fact it will refuse, exactly like a real secondary. From the mongosh you have connected to the primary (note the prompt string), add two new documents to the items collection. In normal MongoDB behavior, this will be created with default settings as needed:

rs0 [direct: primary] test> use shop
rs0 [direct: primary] shop> db.items.insertMany([
     { _id: 1, name: "widget", qty: 10 },
     { _id: 2, name: "gadget", qty: 20 }
])
{
  acknowledged: true,
  insertedIds: { '0': 1, '1': 2 }
}

Then change one of them, so there is some history to look at:

rs0 [direct: primary] shop> db.items.updateOne(
     { _id: 1 },
     { $set: { qty: 99 } }
)
{
  acknowledged: true,
  matchedCount: 1,
  modifiedCount: 1
}

Give it a second or two. Now open a fourth terminal, connect mongosh to DumboDB on port 27018, and look at the same collection:

$ mongosh mongodb://localhost:27018
rs0 [direct: secondary] test> use shop
rs0 [direct: secondary] shop> db.items.find().sort({ _id: 1 })
[
  { _id: 1, name: 'widget', qty: 99 },     // the update arrived
  { _id: 2, name: 'gadget', qty: 20 }
]

The Part MongoDB Can’t Do#

Every replication event from MongoDB into DumboDB becomes a commit. dumboLog is used to walk the commits and print information about them. Using the patch flag we can see what changed:

rs0 [direct: secondary] shop> db.runCommand({ dumboLog: 1, limit: 2, patch: 1 })
{
  commits: [
    {
      commitId: '4q7nek16k1sasj1hpu1iunsqab0g5kmt',
      refs: [ 'HEAD', 'main' ],
      parent1: 'kjgka0tuhe5licg55kp05bffdeb0cb7n',
      message: 'MongoDB replication publication 7ad1ee468c09bbcac9d4360edaeb1287f2f90ed031df25a6a22fa9073a4dc186',
      timestamp: ISODate('2026-09-21T22:18:31.000Z'),
      author: 'MongoDB Replication <replication@dumbodb>',
      committer: 'MongoDB Replication <replication@dumbodb>',
      committerTimestamp: ISODate('2026-09-21T22:18:31.283Z'),
      changes: [
        {
          type: 'collection',
          name: 'items',
          status: 'modified',
          documents: {
            added: [],
            removed: [],
            modified: [
              {
                _id: 1,
                diff: [
                  { type: 'modified', path: '$.qty', from: 10, to: 99 }
                ]
              }
            ]
          },
          indexes: { added: [], removed: [], modified: [] },
          metadata: {}
        }
      ]
    },
    {
      commitId: 'kjgka0tuhe5licg55kp05bffdeb0cb7n',
      parent1: '1cke6ej1803tmfdoi7s0ii9ii99g4tq2',
      message: 'MongoDB replication publication e15621942dc2b4769bc41f3eca1eb5027b1832ba60c201226846f2e5b8677996',
      timestamp: ISODate('2026-09-21T22:18:24.000Z'),
      author: 'MongoDB Replication <replication@dumbodb>',
      committer: 'MongoDB Replication <replication@dumbodb>',
      committerTimestamp: ISODate('2026-09-21T22:18:24.478Z'),
      changes: [
        {
          type: 'collection',
          name: 'items',
          status: 'modified',
          documents: {
            added: [
              { _id: 2, name: 'gadget', qty: 20 },
              { _id: 1, name: 'widget', qty: 10 }
            ],
            removed: [],
            modified: []
          },
          indexes: { added: [], removed: [], modified: [] },
          metadata: {}
        }
      ]
    }
  ],
  next: [ '1cke6ej1803tmfdoi7s0ii9ii99g4tq2' ],
  ok: 1
}

Read that from the top. The newest commit is the updateOne, and DumboDB recorded it as a modification to _id: 1 with a field-level diff: $.qty went from: 10 to: 99. The commit before it is the insertMany, recorded as two added documents.

And there you have it. We took regular traffic against a MongoDB server, and a read-only secondary DumboDB instance turned that into an event record for every update to the primary server. Never scratch your head again about how a document got into the state it’s in!

What’s Next#

As stated above, this feature doesn’t currently support server topologies where authentication and authorization are enabled. This is obviously a blocker for any realistic setup, so we will get that addressed before too long. There are also features we could consider such as applying a diff from a DumboDB server back into your MongoDB instance, enabling offline work that doesn’t stay trapped in your DumboDB replica.

What would you like us to work on in the realm of version controlled databases? Come nerd out with us on our Discord server!