Modelcore
← Back to Blog
4 min readShane Scranton

Deterministic IDs: Why Command Replay Works

How moving ID allocation to command creation makes undo/redo and multiplayer reliable.

engineering

If you've ever undone an extrusion and then redone it, only to find that your material assignments vanished or your selection broke, you've hit a subtle but notorious CAD problem: non-deterministic IDs.

The geometry engine creates entities (faces, edges, vertices, solids) and each needs an identifier. If those identifiers change every time a command runs, undo/redo breaks. Multiplayer diverges. Command logs become useless.

This post explains why deterministic ID allocation matters and how we solved it.

The problem

Consider a simple extrusion command. When it executes, the geometry engine creates new entities: side faces, new edges, a top face, and a solid. Each needs an ID.

The naive approach:

const newFaceId = crypto.randomUUID() // Different every time!

This works for single-user, no-undo workflows. But:

  1. Undo/redo breaks. After undo, the command is re-executed on redo. New random IDs are generated. Now the solid has a different ID than before.
  2. Multiplayer diverges. User A and User B execute the same command. They get different IDs. Selection sync, lock references, and material bindings all break.
  3. Replay fails. Command logs can't be replayed to reconstruct a scene because IDs aren't reproducible.

The fix: ID allocation at command creation

The solution is to move ID allocation from execution time to command creation time. When a command is created, it includes a stable seed:

{
  type: 'geometry.extrude',
  payload: {
    faceId: 'face-123',
    distance: 2,
    idSeed: 'cmd-a7f2'  // Stable seed for this command instance
  }
}

At execution time, the engine creates a seeded ID allocator from this seed. The allocator produces IDs deterministically based on the seed and the order of allocation calls.

Same seed, same IDs. Every time.

Why this matters for undo/redo

Undo in a command-based system typically works by storing inverse operations or snapshots. But the cleanest approach is to store the command itself and re-execute on redo.

With deterministic IDs:

  1. User executes extrusion: a solid is created with a predictable ID
  2. User clicks undo: command is reversed, solid is deleted
  3. User clicks redo: same command executes, same solid ID is created

The rest of the app (selection, materials, scene graph references) doesn't need to know the ID changed, because it didn't.

Why this matters for multiplayer

In Modelcore's multiplayer architecture, we broadcast commands to collaborators. When User A draws a rectangle, the command is sent to User B's client, which executes it locally.

With deterministic IDs, User A and User B end up with identical geometry. The test is simple:

const engineA = new GeometryEngine()
const engineB = new GeometryEngine()

const cmd = { type: 'geometry.extrude', payload: { idSeed: 'seed-1', ... } }

execute(engineA, cmd)
execute(engineB, cmd)

expect(engineA.getSolids()[0].id).toEqual(engineB.getSolids()[0].id)

We don't sync state; we sync commands. And commands produce identical results because IDs are deterministic.

The allocator patterns

We support two approaches:

Seeded allocator: The command includes an idSeed string. All IDs are derived deterministically from it. This is the common case.

Explicit allocator: For operations with a known, fixed entity count, the command can pre-allocate specific IDs. This is useful for certain low-level operations.

Both ensure that re-executing the same command produces the same IDs.

The architectural decision

The key insight: ID allocation is a concern of the command layer, not the geometry layer. The geometry engine accepts an allocator; it doesn't decide how IDs are generated.

This separation means the engine stays pure and testable, while the command layer controls reproducibility.

Outcomes

With deterministic IDs in place:

  • Undo/redo is reliable. Commands can be freely replayed without ID drift.
  • Multiplayer produces identical scenes. Command broadcast works because execution is deterministic.
  • Command logs are meaningful. A sequence of commands can reconstruct any scene state.
  • Testing is tractable. We can assert on specific IDs in integration tests.

It's a small shift (moving ID allocation earlier in the pipeline) but it unlocks correctness guarantees that would otherwise require complex state reconciliation.

See how this fits into our larger collaboration story in Introducing Multiplayer.