1---
2title: Circular Bindings
3description: How to model two services that reference each other (Worker A ↔ Worker B, Lambda ↔ Lambda) using tagged classes and Layers.
4---
5
6import DAG from "../../../components/DAG.astro";
7
8Real systems have cycles. A web Worker calls an internal Worker for
9auth; the internal Worker calls back into the web Worker for
10billing. Two Lambdas trigger each other through a queue.
11
12Most IaC engines reject these — you can't create A before B if A
13needs B's URL, but B needs A's URL too. Alchemy resolves the
14deadlock by separating **identity** (a class used as a Tag) from
15**implementation** (a Layer attached via `.make()`).
16
17This guide builds on [Bindings](/infrastructure-as-effects/binding) and
18[Layers](/infrastructure-as-effects/layers): two Workers that call each
19other, one step at a time.
20
21## Sketch the cycle
22
23The goal: two Workers, `A` and `B`. Each one delegates half of its
24work to the other.
25
26```
27GET / → A ──rpc──▶ B
28 ◀──rpc───
29```
30
31Naively you'd write `import { B } from "./B.ts"` inside `A.ts` and
32`import { A } from "./A.ts"` inside `B.ts`. The imports succeed at
33the module level, but at deploy time neither Worker can be created
34first — each needs the other's URL.
35
36The fix is to import only the **Tag** (cheap, side-effect free)
37across the cycle, and keep each Worker's **runtime implementation**
38behind a `.make()` call that runs only when the Stack provides it.
39
40## Create A as a Tag
41
42Start `src/A.ts` with just the class — no runtime yet. The class
43extends `Cloudflare.Worker<A, Shape>()(...)`, which produces a typed
44identifier you can `yield*` from anywhere. The second type parameter
45declares the shape A's runtime will expose — here, a `work` method.
46
47```typescript
48// src/A.ts
49import * as Cloudflare from "alchemy/Cloudflare";
50import * as Effect from "effect/Effect";
51
52export class A extends Cloudflare.Worker<A, { work: () => Effect.Effect<string> }>()("A") {}
53```
54
55`A` is now a Tag. It carries the type of A's interface, but no
56implementation — importing it from another file does **not** force
57any runtime code to load, because there is no implementation yet.
58This is what makes the cycle resolvable.
59
60## Create B as a Tag
61
62Do the same for `B`. Both files only declare identity and shape at
63this point — no runtime.
64
65```typescript
66// src/B.ts
67import * as Cloudflare from "alchemy/Cloudflare";
68import * as Effect from "effect/Effect";
69
70export class B extends Cloudflare.Worker<B, { work: () => Effect.Effect<string> }>()("B") {}
71```
72
73Now `A` and `B` can freely `import` each other — neither side
74triggers any runtime code by importing the other's class.
75
76## Add A's runtime, binding B
77
78Attach A's implementation with `A.make(...)`. Inside the Construction phase,
79`yield* Cloudflare.Workers.bindWorker(B)` produces a typed stub whose
80methods dispatch to B's deployed Worker at runtime. A also exposes its
81own `work` RPC method so B can call back into it.
82
83```diff lang="typescript"
84 // src/A.ts
85 import * as Cloudflare from "alchemy/Cloudflare";
86 import * as Effect from "effect/Effect";
87+ import * as HttpServerResponse from "effect/http/HttpServerResponse";
88+ import { B } from "./B.ts";
89
90 export class A extends Cloudflare.Worker<A, { work: () => Effect.Effect<string> }>()("A") {}
91
92+ export default A.make(
93+ { main: import.meta.url },
94+ Effect.gen(function* () {
95+ const b = yield* Cloudflare.Workers.bindWorker(B);
96+ return {
97+ fetch: Effect.gen(function* () {
98+ // delegate half the work to B over the RPC stub
99+ return HttpServerResponse.text(yield* b.work());
100+ }),
101+ work: () => Effect.succeed("A handled its half"),
102+ };
103+ }),
104+ );
105```
106
107The `import { B }` line pulls in B's Tag only. `bindWorker(B)`
108returns a [typed RPC stub](/apis/schemaless) — at plan time it
109registers the service binding; at runtime `b.work()` round-trips to
110B's deployed Worker as a native RPC call.
111
112## Add B's runtime, binding A
113
114Mirror the same pattern in `B.ts`. B's runtime imports A's Tag and
115binds it.
116
117```diff lang="typescript"
118 // src/B.ts
119 import * as Cloudflare from "alchemy/Cloudflare";
120 import * as Effect from "effect/Effect";
121+ import * as HttpServerResponse from "effect/http/HttpServerResponse";
122+ import { A } from "./A.ts";
123
124 export class B extends Cloudflare.Worker<B, { work: () => Effect.Effect<string> }>()("B") {}
125
126+ export default B.make(
127+ { main: import.meta.url },
128+ Effect.gen(function* () {
129+ const a = yield* Cloudflare.Workers.bindWorker(A);
130+ return {
131+ fetch: Effect.gen(function* () {
132+ return HttpServerResponse.text(yield* a.work());
133+ }),
134+ work: () => Effect.succeed("B handled its half"),
135+ };
136+ }),
137+ );
138```
139
140This is the symmetric half of the cycle. `A` imports `B`'s Tag and
141binds it; `B` imports `A`'s Tag and binds it. Neither file needs the
142other's `.make()` to be evaluated.
143
144## Wire both into the Stack
145
146The Stack pulls in both Tags (via `import { A }` / `import { B }`)
147and both Layers (via the default exports). `Effect.provide` attaches
148the Layers to the generator so the Tags resolve to real Workers.
149
150```typescript
151// alchemy.run.ts
152import * as Alchemy from "alchemy";
153import * as Cloudflare from "alchemy/Cloudflare";
154import * as Effect from "effect/Effect";
155import * as Layer from "effect/Layer";
156import ALive, { A } from "./src/A.ts";
157import BLive, { B } from "./src/B.ts";
158
159export default Alchemy.Stack(
160 "AB",
161 { providers: Cloudflare.providers(), state: Cloudflare.state() },
162 Effect.gen(function* () {
163 const a = yield* A;
164 const b = yield* B;
165 return { aUrl: a.url, bUrl: b.url };
166 }).pipe(Effect.provide(Layer.mergeAll(ALive, BLive))),
167);
168```
169
170`yield* A` and `yield* B` only succeed because `ALive` and `BLive`
171are provided. Forget one, and TypeScript flags the missing layer at
172the call site.
173
174## How alchemy resolves the cycle
175
176Under the hood, alchemy plans the cycle in two passes:
177
178<DAG
179 nodes={[
180 { id: "Decl", label: "declare", type: "A & B Tags" },
181 { id: "Pre", label: "precreate", type: "reserve URLs", tone: "accent" },
182 { id: "Create", label: "create", type: "deferred Outputs" },
183 { id: "Wire", label: "update", type: "wire bindings", tone: "accent" },
184 ]}
185 edges={[
186 { from: "Decl", to: "Pre" },
187 { from: "Pre", to: "Create" },
188 { from: "Create", to: "Wire" },
189 ]}
190/>
191
1921. The **Tags** are registered up front, so the graph knows that A
193 and B both exist.
1942. Each provider's [`precreate`](/infrastructure-as-code/provider#precreate) hook
195 reserves the resource (and its physical URL) without needing the
196 other side's outputs.
1973. `create` runs in parallel using deferred Outputs — bindings see
198 `Output<string>` placeholders that resolve later.
1994. A converge pass calls `update` once both sides exist, wiring the
200 real cross-references in. Types stay sound the whole way through
201 because Outputs are typed.
202
203The same pattern works for Lambda↔Lambda, Worker↔Container, or any
204mix — the Tag/Layer split is a property of every Function resource
205([Runtime](/infrastructure-as-effects/runtime)), not just Workers.
206
207## When you don't need this
208
209If your services form a DAG (no cycles), you can keep declaration
210and implementation in one expression:
211
212```typescript
213export default Cloudflare.Worker(
214 "MyWorker",
215 { main: import.meta.url },
216 Effect.gen(function* () {
217 /* ... */
218 }),
219);
220```
221
222The tagged-class pattern only pays off when something else needs to
223reference the Worker before its implementation is in scope. For
224non-circular reuse across files, see [Layers](/infrastructure-as-effects/layers).
225
226The deferred `Output` mechanism behind the two-phase plan is covered
227in [Inputs & Outputs](/infrastructure-as-code/outputs).
228
229## Where next
230
231- [Inputs & Outputs](/infrastructure-as-code/outputs) — the deferred references that make the cycle resolvable.
232- [Providers](/infrastructure-as-code/provider) — the `precreate` hook that reserves resources up front.