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 dedupe 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 yet.
We’re not deduping retries yet.
We’re not building queues yet.
Now, is one move:
take a key -> route to one Durable Object instance -> handle the request there
And then we prove it with the simplest signal possible: an in memory counter.
The counter is not 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.
And 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:
A Worker is your HTTP entrypoint. Every request hits the Worker first.
Inside the Worker you’ll see an env object. Think of env as “injected dependencies”:
bindings you declare in wrangler.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 is not one object. It’s a factory for many objects.
From a namespace, you do three steps:
-
Pick a key (like
"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 the “weird but powerful” part is: 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
- Worker: your HTTP entrypoint. Every request hits this first.
-
env: runtime-injected dependencies (bindings + secrets) defined inwrangler.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 (from
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.jsoncthat 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 yet. We’re not building 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) -
src/routes/day01.ts(route definition) -
src/handlers/day01Hello.ts(handler logic: key -> DO) -
src/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" -> creates env.COORDINATORS in 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
This layout keeps routes clean and pushes logic into handlers.
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<HonoEnv>();
// 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<HonoEnv>) {
// 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);
}
This is the entire “pattern”:
key -> id -> stub -> forward
That’s why Durable Objects are powerful: you get a coordinator per key by construction.
Notice what we didn’t do:
- no shared in memory map in the Worker
- no cache
- no lock
- no database
We only routed the request to the right place.
Step 3 — The Durable Object itself (stateful core)
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)
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?
Because wrangler.jsonc says class_name: "Coordinator", and Wrangler needs that class to be exported by the built script.
Try it (2–5 minutes)
Repository:
https://github.com/Pillin/Durable-Objects-30days
Run:
npm install
npx wrangler dev
then:
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: each key increments independently.
Rule
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.
Top comments (0)