1---
2title: Circular references, without the deadlock
3date: 2026-04-25
4draft: true
5excerpt: Two Workers can call each other. A Lambda's role and the KMS key it uses can grant each other access. Alchemy plans these as graphs with cycles — at both the runtime layer and the type system layer.
6---
7
8Say you have two services that need to talk to each other.
9A web app handles login, an internal service handles
10billing, and each one calls the other.
11
12To deploy them, you need to tell each service where to
13find the other. Easy enough if you're deploying by hand —
14deploy the first, copy its URL, paste it into the second,
15deploy that, copy its URL back into the first. Done.
16
17But you want this in your infra code, so it's repeatable.
18And now you have a problem: which one does the deployer
19create first? Whichever you pick, you don't yet have the
20other one's URL to give it.
21
22This is a **circular dependency**, and most infrastructure
23tools refuse to handle it. They'll plan your resources in
24order, and the moment they detect a cycle they stop and
25ask you to break it manually.
26
27Alchemy doesn't. It plans the cycle in two passes.
28
29## The observation that makes it work
30
31When two resources depend on each other, they almost never
32need the *whole* other resource. They need one piece of
33identifying information — a URL, an ARN, a name. Something
34short and stable that's known the moment the resource
35exists, even before it's fully configured.
36
37That's the wedge. If we can produce identifiers before we
38do the rest of the work, we can plan the cycle as two
39passes:
40
411. Reserve the identifiers for both resources.
422. Now that both identifiers exist, finish configuring each
43 one — including the cross-reference to the other side.
44
45Alchemy gives providers a lifecycle hook for step 1 called
46**`precreate`**, and a regular lifecycle hook for step 2
47called **`reconcile`**.
48
49## `precreate` — reserving identifiers before reconcile
50
51When the planner sees a cycle, it asks each participating
52resource: *can you produce your stable identifiers without
53needing the other side?* If the provider implements
54`precreate`, the answer is yes.
55
56Here's the Cloudflare `Worker` provider's `precreate`
57(abbreviated from [`Worker.ts`](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Cloudflare/Workers/Worker.ts)):
58
59```typescript
60precreate: Effect.fn(function* ({ id, news, session }) {
61 const name = yield* createWorkerName(id, news.name);
62
63 // Upload a placeholder script. Just enough to reserve the
64 // worker name and any Durable Object class IDs the cycle
65 // might reference.
66 yield* putScript({
67 accountId,
68 scriptName: name,
69 metadata: { mainModule: "main.js", /* ...DO stubs... */ },
70 files: [new File([placeholderScript], "main.js")],
71 });
72
73 return {
74 workerId: name,
75 workerName: name,
76 url: undefined, // not yet — filled in by reconcile
77 durableObjectNamespaces,
78 // ...
79 } satisfies Worker["Attributes"];
80}),
81```
82
83The placeholder script is a no-op. It returns
84`"Alchemy worker is being deployed..."`. That's fine — its
85job isn't to handle traffic, it's to reserve a script name
86and (if there are Durable Objects) generate stable namespace
87IDs that other resources can refer to.
88
89In alchemy v1 this was an explicit `WorkerStub` resource you
90had to declare in your stack. In v2 it's invisible — the
91planner detects the cycle, calls `precreate` on the
92participating resources first, and then runs `reconcile` on
93all of them with the cross-references resolved.
94
95For [`AWS/S3/Bucket.ts`](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/AWS/S3/Bucket.ts)
96the `precreate` is even shorter — it just calls
97`ensureBucketExists`, since for the BucketPolicy → Lambda
98cycle all the other side needs is the bucket ARN, which is
99known the moment the bucket exists.
100
101If a provider *doesn't* implement `precreate`, the planner
102still works — it just can't participate in a cycle. You'd
103get the same DAG-only behavior as Terraform. `precreate` is
104an opt-in for the resources where the cycle pattern matters.
105
106## The type-system problem
107
108Engine support is half the battle. The other half is
109authoring code that *expresses* the cycle without the
110TypeScript compiler exploding.
111
112The naive thing to write looks like this:
113
114```typescript
115// src/A.ts
116import { B } from "./B.ts";
117
118export const A = Cloudflare.Worker("A", { main: import.meta.filename },
119 Effect.gen(function* () {
120 const b = yield* Cloudflare.Worker.bind(B);
121 return { fetch: ... };
122 }),
123);
124
125// src/B.ts
126import { A } from "./A.ts";
127
128export const B = Cloudflare.Worker("B", { main: import.meta.filename },
129 Effect.gen(function* () {
130 const a = yield* Cloudflare.Worker.bind(A);
131 return { fetch: ... };
132 }),
133);
134```
135
136Three things go wrong:
137
1381. **Bundle bloat.** Each Worker's `import` pulls in the
139 other Worker's entire implementation. When you bundle `A`
140 for deployment, you also bundle `B`'s handler code — and
141 vice versa. The Cloudflare Worker that's supposed to be
142 small now contains both implementations.
143
1442. **Circular type inference.** TypeScript needs `B`'s type
145 to type-check `A`, but `B`'s type depends on `A`'s type
146 (because the Effect inside `B` references `A`). The
147 compiler bails with:
148
149 > `'A' implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.ts(7023)`
150
1513. **Runtime initialization order.** Even if the types
152 resolved, the module loader has to execute *something*
153 first. Whichever module loads first sees `undefined` on
154 the other side.
155
156## Tag-based design
157
158The fix is to separate **identity** from **implementation**.
159The class is the Tag — it carries the type, the resource ID,
160the binding contract — and it's cheap to import. The
161implementation is attached via a separate `.make()` call
162that runs only when the Stack provides it.
163
164```typescript
165// src/A.ts — just the Tag
166import * as Cloudflare from "alchemy/Cloudflare";
167
168export class A extends Cloudflare.Worker<A>()("A", {
169 main: import.meta.filename,
170}) {}
171```
172
173That's the whole identity declaration. Importing `A` from
174another file gets you the type and the resource ID. It does
175*not* pull in any handler code — there is no handler code
176yet. The class is the Tag.
177
178The runtime piece is a second file-level export:
179
180```diff lang="typescript"
181 // src/A.ts
182 import * as Cloudflare from "alchemy/Cloudflare";
183+ import * as Effect from "effect/Effect";
184+ import { B } from "./B.ts";
185
186 export class A extends Cloudflare.Worker<A>()("A", {
187 main: import.meta.filename,
188 }) {}
189
190+ export default A.make(
191+ Effect.gen(function* () {
192+ const b = yield* Cloudflare.Worker.bind(B);
193+ return {
194+ fetch: Effect.gen(function* () {
195+ return yield* b.fetch(new Request("https://b/work"));
196+ }),
197+ };
198+ }),
199+ );
200```
201
202The `import { B }` here imports B's Tag only — its identity
203and binding contract. `Worker.bind(B)` returns a typed
204callable; at plan time the URL is an `Output<URL>`
205placeholder, at runtime it's a real `fetch` stub.
206
207`B.ts` mirrors the pattern: imports `A`'s Tag, binds it
208inside its own `.make()`. Each file's `Effect.gen` references
209the *other* class as a Tag, not as a value that has to be
210initialized. The bundler sees the Tag import as type-only
211and tree-shakes it from the runtime bundle. The TypeScript
212compiler can resolve the types because each Tag is a single
213class declaration with no dependencies on the other side's
214implementation.
215
216### Why `.make()` and not the inline form
217
218For the non-cyclic case Alchemy lets you write the resource
219and its implementation in a single expression:
220
221```typescript
222export default Cloudflare.Worker("MyWorker", { main: import.meta.filename },
223 Effect.gen(function* () { /* ... */ }),
224);
225```
226
227This is the convenient default — one file, one expression,
228type inference flows in one direction. But the moment the
229generator references *itself*'s tag (`yield* MyWorker`), TS
230trips on the self-reference. The class-form-plus-`.make()`
231splits the declaration from the inferred body, which is
232what unblocks both self-references and cross-references.
233
234The rule of thumb: use the inline form for DAG-shaped
235resources, use the class form when something else
236references the resource before its implementation is in
237scope — including the resource itself.
238
239## How the two pieces compose
240
241At plan time, the engine builds a dependency graph from the
242Stack expression. When it sees a cycle, it routes the
243participating resources through a two-pass lifecycle:
244
245```
2461. precreate — reserve identifiers (placeholder Worker scripts,
247 empty Buckets, KMS keys without policies).
248 Cross-references now have valid Output<…> values.
2492. reconcile — every resource runs with the resolved bindings.
250 For Worker A this means a real putScript with
251 the binding to B's reserved URL; for the KMS
252 key, the full resource policy referencing the
253 role's ARN.
254```
255
256The Tag/Layer split is what makes step 2 *authorable* — the
257generator inside `A.make(...)` can `yield* Worker.bind(B)`
258without anyone needing to evaluate `B`'s `.make()` body. The
259`precreate` hook is what makes step 2 *executable* — by the
260time the generator runs, `B` already has a reserved URL.
261
262You need both layers. Engine support without the Tag/Layer
263split leaves you writing un-bundle-able, un-type-checkable
264code. The Tag/Layer split without engine support leaves you
265with code that type-checks but deadlocks at deploy time.
266
267## When you don't need any of this
268
269If your services form a DAG, write them inline. The
270machinery exists for the cases where the natural shape of
271your system has cycles — bidirectional resource policies,
272mutually-recursive services, queue-mediated callbacks. For
273everything else, the simple form is still simple:
274
275```typescript
276export default Cloudflare.Worker("Web", { main: import.meta.filename },
277 Effect.gen(function* () {
278 const db = yield* Database;
279 return { fetch: /* ... */ };
280 }),
281);
282```
283
284The interesting design choice is that the *engine*
285capability — `precreate` and cycle-aware planning — is
286opt-in per provider, not part of the framework's core
287contract. Most providers don't need it. The ones that do
288(Cloudflare `Worker`, AWS `Bucket`, KMS `Key`, IAM `Role`,
289Lambda `Function`) implement it explicitly, and the planner
290only invokes it when a cycle is actually present.
291
292## Where to go next
293
294- [Guides › Circular Bindings](/infrastructure-as-effects/circular-bindings) —
295 the step-by-step Worker A ↔ Worker B walkthrough.
296- [`Cloudflare/Workers/Worker.ts`](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Cloudflare/Workers/Worker.ts)
297 — the canonical `precreate` reference, including Durable
298 Object namespace reservation.
299- [`AWS/S3/Bucket.ts`](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/AWS/S3/Bucket.ts)
300 — `precreate` for the BucketPolicy ↔ Lambda Role cycle.