Architectural PatternsCoordinator Pattern

Coordinator Pattern

The coordinator pattern describes any architecture where a central node is responsible for sequencing operations across multiple participants. Common examples: Saga Orchestration, Two-Phase Commit (2PC), and Raft Leader-based consensus.


The rule

⚠️

All edges go between the coordinator and individual services. Services are NOT connected to each other directly.

If Service A needs to trigger Service B as part of a saga step, the coordinator issues both calls — first to A, then (after A responds) to B. Services never call each other.

CORRECT:   Coordinator → Service A → Coordinator → Service B
WRONG:     Coordinator → Service A → Service B
                              (Service A calls Service B directly)

Saga Orchestration

In an orchestrated saga, the saga coordinator drives each step: it calls Service A, waits for a reply, then calls Service B, and so on. On failure, it issues compensating transactions in reverse order.

Topology

const topology = new TopologyBuilder(true);
 
const orchestratorGroup = topology.createGroup('orchestrator', {
  label: 'Saga Orchestrator',
});
orchestratorGroup
  .addProcess({ id: 'saga', label: 'Order Saga', state: 'running' })
  .autoResize();
 
const servicesGroup = topology.createGroup('services', { label: 'Domain Services' });
servicesGroup
  .addProcess({ id: 'orders',    label: 'Order Service',    state: 'running' })
  .addProcess({ id: 'payments',  label: 'Payment Service',  state: 'running' })
  .addProcess({ id: 'inventory', label: 'Inventory Service', state: 'running' })
  .addProcess({ id: 'shipping',  label: 'Shipping Service', state: 'running' })
  .autoResize();
 
// All edges are between saga coordinator and services
// Services are NOT connected to each other
topology.connect('saga', 'orders',    { protocol: 'http', label: 'create order' });
topology.connect('saga', 'payments',  { protocol: 'http', label: 'charge card' });
topology.connect('saga', 'inventory', { protocol: 'http', label: 'reserve items' });
topology.connect('saga', 'shipping',  { protocol: 'http', label: 'schedule delivery' });

Animation — happy path

const flow = new FlowBuilder();
 
flow.scenario('happy-path', 'Happy Path', 'All saga steps succeed');
 
flow
  .from('saga').to('orders')
  .showMessage('[STEP 1] Create order record')
  .from('orders').to('saga')
  .showMessage('[OK] Order #42 created')
  .from('saga').to('inventory')
  .showMessage('[STEP 2] Reserve 3x Widget Pro')
  .from('inventory').to('saga')
  .showMessage('[OK] Items reserved (SKU-7)')
  .from('saga').to('payments')
  .showMessage('[STEP 3] Charge $149.97 to card ending 4242')
  .from('payments').to('saga')
  .showMessage('[OK] Payment authorized (txn: ABC123)')
  .from('saga').to('shipping')
  .showMessage('[STEP 4] Schedule delivery for 2026-04-20')
  .from('shipping').to('saga')
  .showMessage('[OK] Shipment #SH-9 scheduled')
  .showMessage('[SAGA COMPLETE] Order #42 fully processed');

Animation — compensating transaction

flow.scenario('compensation', 'Payment Failure', 'Card declined — compensate previous steps');
 
flow
  .from('saga').to('orders')
  .showMessage('[STEP 1] Create order record')
  .from('orders').to('saga')
  .showMessage('[OK] Order #43 created')
  .from('saga').to('inventory')
  .showMessage('[STEP 2] Reserve items')
  .from('inventory').to('saga')
  .showMessage('[OK] Items reserved')
  .from('saga').to('payments')
  .showMessage('[STEP 3] Charge card ...')
  .from('payments').to('saga')
  .showError('[DECLINED] Insufficient funds')
  // Compensating transactions in reverse order
  .from('saga').to('inventory')
  .showMessage('[COMPENSATE] Release reserved items')
  .from('inventory').to('saga')
  .showMessage('[OK] Reservation cancelled')
  .from('saga').to('orders')
  .showMessage('[COMPENSATE] Cancel order record')
  .from('orders').to('saga')
  .showMessage('[OK] Order #43 cancelled')
  .showError('[SAGA FAILED] Order rolled back — payment declined');
 
return flow;

Two-Phase Commit (2PC)

In 2PC, a coordinator sends a Prepare to all participants (Phase 1), collects votes, then sends Commit or Abort to all (Phase 2).

Topology

const topology = new TopologyBuilder(true);
 
const coordinatorGroup = topology.createGroup('2pc', { label: '2PC Coordinator' });
coordinatorGroup
  .addProcess({ id: 'coord', label: 'Transaction Coordinator', state: 'running' })
  .autoResize();
 
const participantsGroup = topology.createGroup('participants', { label: 'Participants' });
participantsGroup
  .addProcess({ id: 'db-a', label: 'Database A', shape: 'cylinder', state: 'running' })
  .addProcess({ id: 'db-b', label: 'Database B', shape: 'cylinder', state: 'running' })
  .addProcess({ id: 'db-c', label: 'Database C', shape: 'cylinder', state: 'running' })
  .autoResize();
 
// Coordinator ↔ each participant
topology.connect('coord', 'db-a', { protocol: 'tcp', label: '2PC' });
topology.connect('coord', 'db-b', { protocol: 'tcp', label: '2PC' });
topology.connect('coord', 'db-c', { protocol: 'tcp', label: '2PC' });

Animation

const flow = new FlowBuilder();
 
flow.scenario('commit', 'Successful 2PC', 'All participants vote YES');
 
flow
  .parallel([
    ['coord', 'db-a'],
    ['coord', 'db-b'],
    ['coord', 'db-c'],
  ])
  .showMessage('[PHASE 1] PREPARE sent to all participants')
  .parallel([
    ['db-a', 'coord'],
    ['db-b', 'coord'],
    ['db-c', 'coord'],
  ])
  .showMessage('[PHASE 1] All participants voted YES')
  .parallel([
    ['coord', 'db-a'],
    ['coord', 'db-b'],
    ['coord', 'db-c'],
  ])
  .showMessage('[PHASE 2] COMMIT sent to all participants')
  .parallel([
    ['db-a', 'coord'],
    ['db-b', 'coord'],
    ['db-c', 'coord'],
  ])
  .showMessage('[PHASE 2] All participants confirmed COMMIT')
  .showMessage('[OK] Distributed transaction committed');
 
return flow;

Raft Leader

In Raft, the elected leader receives all client writes, appends them to its log, and replicates to followers before acknowledging.

Topology

const topology = new TopologyBuilder(true);
 
const raftGroup = topology.createGroup('raft', { label: 'Raft Cluster' });
raftGroup
  .addProcess({ id: 'leader',     label: 'Leader (Node 1)',     state: 'running' })
  .addProcess({ id: 'follower-1', label: 'Follower (Node 2)',   state: 'running' })
  .addProcess({ id: 'follower-2', label: 'Follower (Node 3)',   state: 'running' })
  .autoResize();
 
// All replication is leader → follower (followers do NOT talk to each other)
topology.connect('leader', 'follower-1', { protocol: 'tcp', label: 'AppendEntries' });
topology.connect('leader', 'follower-2', { protocol: 'tcp', label: 'AppendEntries' });
// Client writes go to leader only
topology.connect('client', 'leader', { protocol: 'tcp', label: 'write' });

In Raft, followers never communicate directly — they only receive AppendEntries RPCs from the leader. This maps directly to the coordinator pattern: the leader is the coordinator, followers are participants.