Day 01 - One Key, One Coordinator
Source: Dev.to
Previously in Day 00, we talked about the moment systems become expensive: when the answer is “maybe”
If you haven’t read it, this post is part of a series — start here: Link
A key needs a home
If two requests touch the same thing (an order, a user, a tenant), you want one place to decide what happens.
Not forever. Not globally. Just for that key.
So, we’re going to build the smallest possible proof that this is real:
the same key always routes to the same coordinator.
No rate limiting yet. No deduplication yet. No queue yet.
Just the primitive that makes all of those possible.
Imagine you’re debugging an incident and you ask: “where does order:123 get decided?”
By the end of this post, you’ll have an answer you can point to in code.
The demo you can hold in a few minutes
We’re not building rate limiting, deduping retries, or queues yet.
Now, one move:
take a key → route to one Durable Object instance → handle the request there
…and prove it with the simplest signal possible: an in‑memory counter.
The counter isn’t the point; it’s a flashlight.
We’ll expose a single endpoint:
GET /day/01/hello/:key
Call it with alice, call it with bob, call it with anything.
If alice and bob don’t share a counter, then they don’t share a coordinator.
If they don’t share a coordinator, we have isolation per key — the foundation for everything else.
Demo

A 60‑second Workers mental model
If you’re new to Workers, here’s the only model you need for Day 01:
- Worker – your HTTP entry point. Every request hits the Worker first.
- Inside the Worker you’ll see an
envobject. Think ofenvas “injected dependencies”: bindings you declare inwrangler.jsonc(Durable Objects, KV, D1, secrets, etc.) show up there at runtime.
In this demo the important binding is:
env.COORDINATORS → a Durable Object *namespace*
A namespace isn’t a single object; it’s a factory for many objects. From a namespace you do three steps:
-
Pick a key (e.g.
"alice"). -
Turn it into a stable object ID:
const id = env.COORDINATORS.idFromName(key); -
Get a stub:
const stub = env.COORDINATORS.get(id);
A stub is a reference to the Durable Object instance for that ID, and you talk to that instance by calling stub.fetch(...).
So the Worker is basically a router:
- it reads the key from the request
- it forwards the request to the object that “owns” that key
Where does state live?
- The Worker should stay mostly stateless.
- The Durable Object is where per‑key state/decisions live.
Quick glossary
| Term | Description |
|---|---|
| Worker | Your HTTP entry point. Every request hits this first. |
env | Runtime‑injected dependencies (bindings + secrets) defined in wrangler.jsonc. |
| Binding | A named handle that appears on env (e.g. env.COORDINATORS). |
| Durable Object class | The code you write (e.g. export class Coordinator { … }). |
| Namespace | A “factory” for many Durable Object instances (what you get via the binding). |
| Key / name | The string you choose to represent “the thing” (in Day 01: alice, bob, etc.). |
| Object ID | A stable identifier for one DO instance (created from the key via idFromName(key)). |
| Stub | A reference/proxy to that specific DO instance (namespace.get(id)). |
stub.fetch() | How the Worker sends a request to that DO instance (like calling a mini‑service). |
| State | Per‑object memory and/or persisted data. For Day 01 we use in‑memory state only. |
| Migration | A version tag in wrangler.jsonc that tells Cloudflare about DO schema/class changes over time. |
The one move
Everything today is one mapping:
key → object ID → stub → forward
We’re not solving retries or ordering yet. We’re just proving that a key can have a home.
Repo structure for Day 01
src/
├─ index.ts # entry point + route mounting + DO export
├─ routes/
│ └─ day01.ts # route definition
├─ handlers/
│ └─ day01Hello.ts # handler logic: key → DO
└─ objects/
└─ Coordinator.ts # the Durable Object class
wrangler.jsonc # binding + migration
Config: how the Worker finds your Durable Object
wrangler.jsonc
{
"main": "src/index.ts",
"durable_objects": {
"bindings": [
{ "name": "COORDINATORS", "class_name": "Coordinator" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Coordinator"] }
]
}
What this means
name: "COORDINATORS"→ createsenv.COORDINATORSin your Worker.class_name: "Coordinator"→ tells Cloudflare which exported class implements the DO.migrations→ “I’m introducing a new Durable Object class in this project.”
You don’t run a separate migration command; the migration is applied when you deploy.
Code: how requests flow end‑to‑end
Step 1 — The route (just routing)
src/routes/day01.ts
import { Hono } from "hono";
import type { HonoEnv } from "../types/env";
import { day01HelloHandler } from "../handlers/day01Hello";
export const day01 = new Hono();
// Route shape: /day/01/hello/:key
day01.get("/hello/:key", day01HelloHandler);
This file only defines the URL shape and delegates logic to the handler.
Step 2 — The handler (key → DO instance)
src/handlers/day01Hello.ts
import type { Context } from "hono";
import type { HonoEnv } from "../types/env";
export async function day01HelloHandler(
c: Context
): Promise<Response> {
// Extract the key from the URL
const { key } = c.req.param();
// Turn the key into a stable Durable Object ID
const id = c.env.COORDINATORS.idFromName(key);
// Get a stub for that ID
const stub = c.env.COORDINATORS.get(id);
// Forward the request to the DO instance
const resp = await stub.fetch(c.req.raw);
return resp;
}
Step 3 — The Durable Object (the “home”)
src/objects/Coordinator.ts
export class Coordinator {
// In‑memory counter – just for demonstration
private counter = 0;
async fetch(request: Request): Promise<Response> {
// Increment the counter each time this DO handles a request
this.counter++;
// Return a simple JSON payload showing the key and its count
const url = new URL(request.url);
const key = url.pathname.split("/").pop() ?? "unknown";
return new Response(
JSON.stringify({ key, count: this.counter }),
{
headers: { "Content-Type": "application/json" },
status: 200,
}
);
}
}
Step 4 — Worker entry point (mount the route)
src/index.ts
import { Hono } from "hono";
import { day01 } from "./routes/day01";
import type { HonoEnv } from "./types/env";
const app = new Hono();
// Mount the Day 01 router under /day/01
app.route("/day/01", day01);
export default {
fetch: app.fetch,
};
Running the demo
# 1️⃣ Install dependencies
npm i
# 2️⃣ Start the local dev server
npm run dev
Now visit:
http://localhost:8787/day/01/hello/alice
http://localhost:8787/day/01/hello/bob
You’ll see each key has its own counter, proving that the same key always routes to the same coordinator.
What’s next?
- Add rate‑limiting logic inside
Coordinator. - Add deduplication/retry handling.
- Add a persistent store (KV/SQLite) for durability across restarts.
But for now you have the foundation: a key‑to‑home mapping that lets you build everything else on top.
Step 2 – Router (the “front‑door”)
File: src/routes/day01/hello.ts
import { Hono } from "hono";
export const day01 = new Hono()
.get("/hello/:key", async (c) => {
// 1) Extract the key from the URL
const key = c.req.param("key"); // e.g. "alice" or "bob"
// 2) Map key → Durable Object ID (stable/deterministic)
const id = c.env.COORDINATORS.idFromName(key);
// 3) Get a stub (proxy) to talk to that DO instance
const stub = c.env.COORDINATORS.get(id);
// 4) Forward the request to the DO instance
return stub.fetch(c.req.raw);
});
Pattern
key → id → stub → forward
That’s why Durable Objects are powerful: you get a coordinator per key by construction.
What we don’t do
- No shared in‑memory map in the Worker
- No cache
- No lock
- No database
We only route the request to the right place.
Step 3 – The Durable Object itself (stateful core)
File: src/objects/Coordinator.ts
export class Coordinator {
private inMemoryCount = 0;
constructor(private state: DurableObjectState) {}
async fetch(_req: Request): Promise<Response> {
// This counter lives in memory, per DO instance.
// So `alice` and `bob` won't share it.
this.inMemoryCount++;
return Response.json({
keyHint: this.state.id.toString(),
inMemoryCount: this.inMemoryCount,
note: "In‑memory only (durable storage comes later).",
});
}
}
Why this proves the concept
- Requests for alice hit the alice object instance → counter becomes 1, 2, 3…
- Requests for bob hit the bob object instance → counter becomes 1, 2, 3…
- Same code, different key → isolated state.
Later we’ll replace/extend this state with durable storage for correctness‑critical data.
Step 4 – Entry point + DO export (important)
File: src/index.ts
import { Hono } from "hono";
import { day01 } from "./routes/day01";
// Important: the DO class must be exported from the entry module
export { Coordinator } from "./objects/Coordinator";
const app = new Hono();
app.get("/health", (c) => c.json({ ok: true }));
// Mount day 01 under /day/01
app.route("/day/01", day01);
export default app;
Why export the DO class here?
wrangler.jsonc specifies class_name: "Coordinator". Wrangler needs that class to be exported by the built script, otherwise it cannot instantiate the Durable Object.
Try it (2–5 minutes)
npm install
npx wrangler dev
Then issue a few requests:
curl http://localhost:8787/day/01/hello/alice
curl http://localhost:8787/day/01/hello/alice
curl http://localhost:8787/day/01/hello/bob
curl http://localhost:8787/day/01/hello/bob
Expected result: each key increments its counter independently.
Rule of thumb
If your bug is “multiple things racing over the same entity”, the fix is usually:
Centralize coordination per key (tenant / user / resource).
Durable Objects are one way to get that coordinator.
Stay curious and ship.