---
title: "Coordinator Failure Domains in Durable-Object 2PC"
subtitle: "A methods note on blast-radius control for atomic commit across single-writer entities"
discipline: "Computer Science"
articleType: "Methods"
authors: ["Dima Doronin"]
keywords: ["Distributed Systems", "Two-Phase Commit", "Cloudflare Durable Objects", "Atomic Commit", "Edge Computing"]
date: "2026-07-19T13:19:36.914Z"
---

# Coordinator Failure Domains in Durable-Object 2PC

*A methods note on blast-radius control for atomic commit across single-writer entities*

## Abstract

This methods note studies atomic commit across multiple single-writer entities implemented with Cloudflare Durable Objects. We present a compact two-phase-commit pattern with idempotent RPCs, durable decision logging, alarm-driven recovery, and coordinator fencing. The contribution is architectural rather than theoretical: 2PC remains blocking, but coordinator state can be scoped per transaction to reduce shared-fate blast radius relative to centralized coordination paths. We make safety/liveness assumptions explicit, distinguish 3PC from replicated-consensus approaches, and include an implementation sketch with stated invariants.

This manuscript focuses on one technical question: how to run atomic commit across independent single-writer entities while reducing shared-fate coordination risk.

Cloudflare Durable Objects (DOs) are relevant because they combine:
- single-writer execution per object identity,
- transactional durable storage,
- runtime-driven retry/recovery via alarms.

These runtime features do not remove classical distributed-transaction trade-offs, but they provide a practical substrate for implementing two-phase commit (2PC) with explicit failure-domain boundaries (\[D1\]).

## 1. Model and guarantees

For cross-entity operations (for example, transferring value between two accounts), we use standard atomic-commit framing (\[Gray 1978\], \[Bernstein et al. 1987\]):

- AC1 Agreement.
- AC2 Validity.
- AC3 Stability.
- AC4 Termination under stated liveness assumptions.

2PC provides AC1-AC3 safety. Termination is conditional: participants may remain in-doubt until coordinator recovery makes the durable decision reachable (\[Gray 1978\], \[Skeen 1981\]).

We assume crash-recovery and eventual message delivery. Under this model, safety remains intact while liveness is eventual.

Non-blocking alternatives require care in terminology:
- 3PC is timeout-based and depends on stronger timing assumptions (\[Skeen 1981\]).
- Paxos Commit avoids coordinator-single-point blocking by replication and consensus (\[Gray & Lamport 2006\]).

## 2. Minimal 2PC implementation sketch over DOs

### 2.1 Participant entity

```typescript
import { DurableObject } from "cloudflare:workers";

type Vote = { ok: true; fence: number } | { ok: false; reason: string };

export class Account extends DurableObject {
  sql: SqlStorage;

  constructor(private ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.sql = this.ctx.storage.sql;
    this.ctx.blockConcurrencyWhile(async () => {
      this.sql.exec(`
        CREATE TABLE IF NOT EXISTS account(k TEXT PRIMARY KEY, balance INTEGER NOT NULL);
        INSERT OR IGNORE INTO account(k, balance) VALUES('bal', 0);
        CREATE TABLE IF NOT EXISTS holds(
          txn TEXT PRIMARY KEY,
          delta INTEGER NOT NULL,
          state TEXT NOT NULL,   -- PREPARED | COMMITTED | ABORTED
          fence INTEGER NOT NULL
        );
      `);
    });
  }

  private balance(): number {
    return Number(this.sql.exec(`SELECT balance FROM account WHERE k='bal'`).one().balance);
  }

  prepare(txn: string, delta: number, fence: number): Vote {
    const existing = this.sql.exec(`SELECT state, fence FROM holds WHERE txn=?`, txn).toArray()[0];
    if (existing) {
      const persistedFence = Number(existing.fence);
      if (fence < persistedFence) return { ok: false, reason: "stale-coordinator" };
      if (existing.state === "ABORTED") return { ok: false, reason: "already-aborted" };
      return { ok: true, fence: persistedFence };
    }

    const projected = this.balance() + delta;
    if (projected < 0) {
      this.sql.exec(`INSERT INTO holds(txn,delta,state,fence) VALUES(?,?,'ABORTED',?)`, txn, delta, fence);
      return { ok: false, reason: "insufficient-funds" };
    }

    this.sql.exec(`INSERT INTO holds(txn,delta,state,fence) VALUES(?,?,'PREPARED',?)`, txn, delta, fence);
    return { ok: true, fence };
  }

  commit(txn: string, fence: number): { applied: boolean } {
    const h = this.sql.exec(`SELECT delta, state, fence FROM holds WHERE txn=?`, txn).toArray()[0];
    if (!h) return { applied: false };
    if (h.state === "COMMITTED") return { applied: true };
    if (h.state === "ABORTED") return { applied: false };
    if (fence < Number(h.fence)) return { applied: false };

    this.ctx.storage.transactionSync(() => {
      this.sql.exec(`UPDATE account SET balance = balance + ? WHERE k='bal'`, Number(h.delta));
      this.sql.exec(`UPDATE holds SET state='COMMITTED' WHERE txn=?`, txn);
    });
    return { applied: true };
  }

  abort(txn: string, fence: number): { released: boolean } {
    const h = this.sql.exec(`SELECT state, fence FROM holds WHERE txn=?`, txn).toArray()[0];
    if (h && fence < Number(h.fence)) return { released: false };
    if (h && h.state === "PREPARED") {
      this.sql.exec(`UPDATE holds SET state='ABORTED' WHERE txn=?`, txn);
      return { released: true };
    }
    this.sql.exec(`INSERT OR IGNORE INTO holds(txn,delta,state,fence) VALUES(?,0,'ABORTED',?)`, txn, fence);
    return { released: false };
  }
}
```

Participant invariant: for each `txn`, at most one terminal state is persisted; epochs lower than the stored fence are rejected.

### 2.2 Coordinator entity

```typescript
import { DurableObject } from "cloudflare:workers";

interface Leg { entity: string; delta: number; }

export class TxnCoordinator extends DurableObject {
  constructor(private ctx: DurableObjectState, private env: Env) { super(ctx, env); }

  async run(txn: string, legs: Leg[]): Promise<"committed" | "aborted"> {
    const phase = await this.ctx.storage.get<string>("phase");
    if (phase === "DONE_COMMIT") return "committed";
    if (phase === "DONE_ABORT") return "aborted";

    const fence = (await this.ctx.storage.get<number>("fence") ?? 0) + 1;
    await this.ctx.storage.put({ txn, legs, fence, phase: "PREPARING" });

    const votes = await Promise.allSettled(legs.map(l => this.stub(l.entity).prepare(txn, l.delta, fence)));
    const allYes = votes.every(v => v.status === "fulfilled" && v.value.ok);
    const decision = allYes ? "COMMIT" : "ABORT";

    // durable decision precedes phase-2 RPCs
    await this.ctx.storage.put("phase", decision);
    await this.ctx.storage.setAlarm(Date.now() + 2_000);

    await this.finish(txn, legs, decision, fence);
    return decision === "COMMIT" ? "committed" : "aborted";
  }

  private async finish(txn: string, legs: Leg[], decision: string, fence: number) {
    const acked = new Set(await this.ctx.storage.get<string[]>("acked") ?? []);
    await Promise.allSettled(legs.map(async l => {
      if (acked.has(l.entity)) return;
      if (decision === "COMMIT") await this.stub(l.entity).commit(txn, fence);
      else await this.stub(l.entity).abort(txn, fence);
      acked.add(l.entity);
    }));
    await this.ctx.storage.put("acked", [...acked]);

    if (acked.size === legs.length) {
      await this.ctx.storage.deleteAlarm();
      await this.ctx.storage.put("phase", decision === "COMMIT" ? "DONE_COMMIT" : "DONE_ABORT");
    }
  }

  async alarm() {
    const phase = await this.ctx.storage.get<string>("phase");
    if (!phase || phase.startsWith("DONE")) return;

    const m = await this.ctx.storage.get(["txn", "legs", "fence"]);
    const txn = m.get("txn") as string;
    const legs = m.get("legs") as Leg[];
    const fence = m.get("fence") as number;

    const decision = phase === "COMMIT" ? "COMMIT" : "ABORT";
    await this.ctx.storage.setAlarm(Date.now() + 5_000);
    await this.finish(txn, legs, decision, fence);
  }

  private stub(entity: string) {
    const id = this.env.ACCOUNTS.idFromName(entity);
    return this.env.ACCOUNTS.get(id) as unknown as {
      prepare(t: string, d: number, f: number): Promise<Vote>;
      commit(t: string, f: number): Promise<{ applied: boolean }>;
      abort(t: string, f: number): Promise<{ released: boolean }>;
    };
  }
}
```

## 3. Blast-radius interpretation

This design does not make 2PC non-blocking. It does constrain where blocking lives: at the transaction's participants and coordinator record, rather than at a shared global coordination substrate.

A simple approximation for transaction blocking probability is:

$P_{blocked} = 1 - (1 - p)^k$

where `k` is the number of entities touched by a transaction and `p` is marginal per-entity unavailability over the interval. This expression is an intuition aid, not a predictive reliability model.

## 4. Conclusion

Durable Objects do not change the fundamentals of atomic commit. They provide a practical implementation style that keeps coordinator state durable, explicit, and locally scoped, reducing shared-fate blast radius while preserving classical 2PC trade-offs.

## References

- \[Bernstein et al. 1987\] Bernstein, P. A., Hadzilacos, V., & Goodman, N. (1987). *Concurrency Control and Recovery in Database Systems.* Addison-Wesley.
- \[Gray 1978\] Gray, J. (1978). Notes on Data Base Operating Systems. In *Operating Systems: An Advanced Course*, LNCS 60, 393-481. Springer.
- \[Skeen 1981\] Skeen, D. (1981). Nonblocking Commit Protocols. *Proc. ACM SIGMOD*, 133-142.
- \[Gray & Lamport 2006\] Gray, J., & Lamport, L. (2006). Consensus on Transaction Commit. *ACM TODS* 31(1), 133-160.
- \[Herlihy & Wing 1990\] Herlihy, M. P., & Wing, J. M. (1990). Linearizability. *ACM TOPLAS* 12(3), 463-492.
- \[D1\] Cloudflare. Durable Objects documentation. https://developers.cloudflare.com/durable-objects/
- \[D2\] Cloudflare. Durable Objects release notes. https://developers.cloudflare.com/durable-objects/release-notes/
- \[D3\] Cloudflare Blog. Durable Object Facets in Dynamic Workers (April 2026). https://blog.cloudflare.com/durable-object-facets-dynamic-workers/

