---
title: "Per-Entity Coordination at the Edge"
subtitle: "Actor-style boundaries, derived read models, and Durable Objects as one implementation case."
discipline: "Computer Science"
articleType: "Methods"
authors: ["Dima Doronin"]
affiliations: ["openscience.tech"]
keywords: ["actor model", "Durable Objects", "Cloudflare Workers", "distributed systems", "real-time systems"]
date: "2026-07-17T02:50:10.810Z"
---

# Per-Entity Coordination at the Edge

*Actor-style boundaries, derived read models, and Durable Objects as one implementation case.*

## Abstract

Actor-oriented design remains useful because it ties concurrency to ownership: one runtime entity owns one slice of mutable state, evaluates writes in order, and exports effects to the rest of the system through explicit boundaries. This methods article develops a narrow decision rule for that style of architecture. A per-entity coordination boundary is a good fit when one named entity owns invariant-sensitive state, incoming writes must be evaluated in a meaningful order, and read-optimized projections can be updated after the authoritative decision. The article presents concrete TypeScript patterns for routing, state transitions, and derived read models, then treats Cloudflare Durable Objects as one edge-hosted implementation of that broader design rather than as the design itself.

The actor model remains useful because it turns a synchronization problem into an ownership problem: one actor owns one slice of mutable state, and other participants interact with that state by sending messages rather than by mutating it directly [1, 2]. That framing is still useful in modern web systems because a recurring design problem is deciding where ordered mutation is actually allowed.

This article develops a narrow methods claim rather than a platform recommendation. A per-entity coordination boundary is a good application-level fit when three conditions hold together:

1. One named entity owns the invariant-sensitive state.
2. Writes to that entity must be evaluated in a meaningful order.
3. Read-optimized views of that entity can be maintained outside the hot path without participating in each authoritative write.

If those conditions do not hold, the per-entity boundary is often the wrong abstraction. The purpose of the article is to define that fit criterion precisely enough that a developer can reject it where it does not belong and implement it coherently where it does.

## The boundary being proposed

The key design choice is to map one logical entity to one coordination authority. The entity might be a room, a cart, a ticket, an account, or a live document session. What matters is not the domain label but the shape of the problem. The entity must have local rules whose correctness depends on the order in which mutations are accepted.

That definition is intentionally narrower than "stateful application." Many applications have state without needing a per-entity coordinator. A profile page can usually be stored as a row and updated conventionally. An analytics dashboard may need query power rather than ordered mutation. A per-entity coordinator becomes useful when the central question is not merely what the current value is, but whether the next write is admissible given the entity's current state.

## Why ordering matters

Serialized evaluation is valuable only when the order of accepted operations changes the meaning of the result. A counter that must not oversell inventory, a ticket that may move only through a valid workflow, or a room that must sequence membership changes all depend on more than the final stored value. They depend on the sequence of accepted operations.

In a stateless request architecture, these rules are often implemented indirectly through database locks, optimistic retries, compare-and-swap logic, or application-level deduplication. Those techniques can work, but they distribute the coordination story across several layers. A per-entity coordinator consolidates it. The next mutation is evaluated by the same named authority that owns the current local state.

This is also why the pattern should not be described as a replacement for databases in general. Databases are excellent at persistent storage and query. The coordinator solves a different problem: it gives one entity a place to enforce order-sensitive rules before derived storage is updated. In broader distributed-systems terms, this is consistent with Helland's argument that transactional work should align with smaller entities and that larger system views are commonly assembled asynchronously rather than maintained as one global transaction [3].

## Minimal routing pattern

The first implementation step is to choose coordinator identity from the domain rather than from the transport. A room id, document id, or order id usually expresses authority more clearly than a connection id or request id.

A minimal routing pattern looks like this:

```typescript
export async function routeToCoordinator(request: Request): Promise<Response> {
  const url = new URL(request.url);
  const entityId = url.pathname.split('/')[2];
  const coordinator = await lookupCoordinator(entityId);
  return coordinator.handle(request);
}
```

The code is small because the important decision is architectural, not syntactic. The routing layer resolves the entity key and forwards the request to the coordinator that owns that key. Once that mapping is stable, all invariant-sensitive logic for that entity can be placed behind a single boundary.

This pattern is appropriate only if the identifier refers to something durable in the domain. If the key is merely a transient transport artifact, the boundary usually becomes noisy and difficult to reason about.

## State-transition pattern

Inside the coordinator, the useful abstraction is usually not "store some fields" but "evaluate a transition." The code below illustrates the shape:

```typescript
type Status = 'draft' | 'submitted' | 'approved';

interface TicketState {
  status: Status;
  ownerId: string;
}

function canTransition(state: TicketState, next: Status, actorId: string): boolean {
  if (actorId !== state.ownerId) return false;
  if (state.status === 'draft' && next === 'submitted') return true;
  if (state.status === 'submitted' && next === 'approved') return true;
  return false;
}
```

This example is deliberately simple because the methodological point is simple: the coordinator should evaluate whether the next mutation is allowed before any derived systems are updated. The decisive feature is not the enum itself. It is that authorization and ordering are evaluated against the entity's local state in one place.

For the same reason, methods are often clearer when they are phrased as decisions rather than setters. `approveSubmission()` communicates a governed transition more accurately than `setStatus('approved')` because it leaves room for invariant checks, actor checks, and downstream side effects.

## Derived read models belong elsewhere

The third fit criterion is easy to ignore but operationally important: public or analytical reads do not need to wake the live coordinator if they do not participate in the invariant. Once the coordinator has accepted a transition, it can emit or trigger a derived representation optimized for a different workload.

A common pattern is:

1. The coordinator accepts a local mutation.
2. The coordinator persists the new authoritative state.
3. A queue or follow-up write updates search documents, rankings, timelines, or publication artifacts.

This split matters because coordination-heavy writes and read-heavy discovery queries usually have different performance and storage needs. The coordinator is the right home for the decision. It is rarely the best home for every read path built on top of that decision.

A minimal sketch of this separation is:

```typescript
await storage.put('state', nextState);
await queue.send({
  id: entityId,
  status: nextState.status,
  updatedAt: Date.now(),
});
```

The exact transport may vary, but the principle does not: the derived index is a consequence of the local transition, not the thing that authorizes it.

## A practical real-time pattern

The real-time case that most clearly fits the method is a session-like entity with multiple clients and a shared notion of current truth. A room or collaborative document session typically needs at least four behaviors at once: client admission, current membership or presence, ordered mutation, and broadcast of accepted changes.

Those responsibilities are tightly coupled enough that placing them in one per-entity coordinator usually simplifies the design. Consider the outline below:

```typescript
class RoomCoordinator {
  private members = new Map<string, Connection>();
  private state: DocState = createInitialState();

  async join(userId: string, connection: Connection) {
    this.members.set(userId, connection);
    this.broadcast({ type: 'presence', userId, action: 'join' });
  }

  async applyEdit(userId: string, edit: Edit) {
    const next = applyToState(this.state, edit);
    this.state = next;
    this.broadcast({ type: 'edit', userId, edit });
  }
}
```

This code is illustrative, not complete. It omits validation, persistence, and reconnect handling. Its purpose is to show why the boundary helps. Admission, sequencing, and broadcast all refer to the same live entity, so placing them in one authority reduces the number of independent places where concurrent behavior must be reconstructed after the fact.

That benefit should be described carefully. The coordinator does not remove all distributed-systems complexity. It removes one specific class of ambiguity: who is allowed to serialize changes to this entity right now?

## Durable Objects as one implementation case

Cloudflare Durable Objects are one implementation of this broader pattern, not the pattern itself [4]. Cloudflare documents them as a special kind of Worker with a globally unique name or identifier, attached durable storage private to the object, and a single-threaded execution model [4]. Those properties make them a plausible host for the per-entity coordination boundary described above.

A Durable Object therefore becomes relevant only after the architectural question has already been answered. If the problem genuinely requires one named entity to admit, order, and persist its own mutations, Durable Objects provide one way to implement that boundary on an edge-hosted runtime [4]. If the problem is dominated instead by broad querying, aggregation, or analytics across many entities, the method points elsewhere regardless of platform.

## When the fit fails

The same method can identify poor fits. If the dominant workload is a cross-entity query, a full-text search, or a leaderboard over many rows, the central problem is not local coordination. A database or search index should lead. If the critical invariant spans many entities at once, a single-object boundary may be too narrow to express the real constraint. If one key absorbs disproportionate traffic, the coordinator owning that key becomes the place where all order-sensitive writes must queue, which may be acceptable or may reveal that the entity boundary is too coarse.

These are not exceptions to the method; they are part of it. A per-entity coordinator is not justified by the mere presence of mutable state. It is justified by the presence of a local authority question.

## Design rules

Several implementation rules follow directly from the fit criterion.

1. Choose coordinator identity from a durable domain entity, not from a transport artifact.
2. Keep invariant-sensitive decisions inside the coordinator and phrase them as transitions, admissions, reservations, or approvals rather than raw field updates.
3. Push query-oriented or public-facing representations into derived stores updated after the authoritative decision.
4. Treat queues, caches, and object storage as downstream companions to the coordinator, not as substitutes for its coordination role.
5. Reconsider the boundary if one coordinator must own too many unrelated concerns or if the key attracts enough traffic to dominate the system's write path.

These rules are intentionally concrete because the article's claim is bounded. It does not claim that actor-style per-entity coordination is universally simpler or faster. It claims something narrower: the pattern is a strong fit when one named entity must evaluate order-sensitive writes and can export asynchronous read models to other systems.

## Conclusion

The actor model remains a useful design language because it forces a direct question: which entity owns this mutation, and where is the order of evaluation defined [1, 2]? The methods argument developed here is that per-entity coordination is a good fit when one named entity owns the invariant, incoming writes must be evaluated in order, and broader read views can be projected elsewhere after the authoritative decision.

On platforms that provide named single-threaded stateful runtimes, including Durable Objects, that boundary can be implemented directly [4]. On platforms that do not, the same architectural criterion still applies even if the implementation mechanism differs. The important result is therefore not product adoption guidance but a selection rule: use a per-entity coordinator when the difficult part of the system is local ordered mutation, and reject it when the difficult part is broad query or cross-entity aggregation.

## References

[1] Hewitt, C., Bishop, P. & Steiger, R. A Universal Modular ACTOR Formalism for Artificial Intelligence. Proceedings of the 3rd International Joint Conference on Artificial Intelligence, 235-245 (1973).
[2] Agha, G. Actors: A Model of Concurrent Computation in Distributed Systems. MIT Press (1986).
[3] Helland, P. Life beyond Distributed Transactions: an Apostate's Opinion. CIDR (2007). https://www.cidrdb.org/cidr2007/papers/cidr07p15.pdf
[4] Cloudflare. What are Durable Objects? Cloudflare Docs. https://developers.cloudflare.com/durable-objects/what-are-durable-objects/ Accessed 2026-07-16.

