SystemsGENERALTEAM-SPECIFICILLUSTRATIVE

Inside and Outside the System

The payment provider, the email sender, object storage and the shipping carrier are not part of the store. Drawing the boundary — what we control, what we merely call — is the first step of every design, because everything outside the line can fail without asking us.

The moveWorked exampleNext questions

The situation, the reflex, and why it stalls

Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.

The question

Where does our system end, and which of the things the store depends on are on the other side of that line?

The situation

My architecture sketch has a box for payments, a box for emails and a box for images, next to the boxes for cart and orders. They all look the same. But I keep being asked "what if payments is down?" and I do not know how to answer, because in my drawing payments is just another box I built.

The reflex

Draw all the components. Every box the store needs goes on the diagram, connected by arrows, and the diagram is complete when nothing is missing. Payments, email and storage are components like any other, so they get the same kind of box.

Why it stalls

The diagram cannot answer "what if payments is down?" because it has no notion of a box that can be down without us. Every box is drawn as though we could open it, fix it and redeploy it, and for three of them that is false.

What the reflex produces — and fails to produce
  • The diagram cannot answer "what if payments is down?" because it has no notion of a box that can be down without us. Every box is drawn as though we could open it, fix it and redeploy it, and for three of them that is false.
  • The design treats the provider's API like an internal function: called synchronously, assumed to return, assumed to be fast. The first slow afternoon at the provider makes every checkout hang, and the root cause is a drawing convention.
  • Responsibilities blur. Because the email box looks internal, "did the confirmation send?" is assumed to be knowable and instant; the code that awaits it in the checkout transaction is written in good faith and is wrong.
  • Costs are invisible. Internal boxes are free to call; external ones are metered, rate-limited and contractually versioned, and none of that is on the diagram, so the design decides to call the shipping quote API on every cart render.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

Precisely enough to apply it to a problem you have never seen — not a slogan.

  • Draw the line before the boxes. Inside: everything you can change, deploy and observe — your code, your database, your workers. Outside: everything you can only *call* — the payment provider, the email service, object storage, the shipping carrier, the customer's browser, the customer. The test is "if it misbehaves, can I fix it, or only wait?"
  • For each thing outside, name the *contract* — what you send, what you get back, how long it may take, what it promises — and note that the contract is theirs to change. For each thing inside that talks to it, that is a boundary, and boundaries get an adapter, a timeout and a failure plan by default (Where Does My System End?).
  • Classify what crosses the line: a call you wait for (payment authorisation), a call you fire and forget (confirmation email), a message they send you (webhook), a file you store there (product image). Each shape has a different failure story, and the diagram should show which is which.
  • Redraw. Outside things get a different shape and sit outside the line; every arrow across the line carries a label with its shape. Now "what if payments is down?" is a question about one labelled arrow, and it has an answer (External Systems Fail).

The line, drawn

The diagram is the sketch after the move. Everything inside can be changed and redeployed; everything outside can only be called, and each crossing arrow says what shape the crossing is. The customer's browser is outside on purpose.

inbound requestswait-for: authoriseinbound: settlement webhookfire-and-forget: confirmationstored object via signed URLwait-for: quote (cached)inbound: tracking updatesCustomer's browser (outside)Background workerObject storage (outside)Email service (outside)Shipping carrier (outside)API + order serviceDatabasePayment provider (outside)
UserLLMAgentToolDataDecisionHumanGuardrail

What crosses, and what each shape commits you to

Four shapes cover nearly every crossing, and the shape decides the failure plan. A wait-for needs a timeout and a user-facing outcome for the timeout. A fire-and-forget needs a retry and a way to notice it never happened. An inbound message needs verification, idempotency and tolerance for order. A stored object needs a plan for the object being gone.

Each outside box also arrives with unknowns, and the board below is what the line produces once the crossings are named: every vague worry about a provider becomes a question about its contract with an experiment against its sandbox.

The outside boxes, as questions
known
  • Payment, email, storage and shipping are outside; the customer's browser is outside; our API, database and worker are inside.
  • Only payment authorise and the shipping quote are wait-for crossings on a user-facing path.
assumed
  • ~The payment provider delivers a webhook for every settlement. To be checked against their documentation — some deliver only on request.
unknown → question → experiment
  1. ? The payment provider.

    becomes When our authorise call times out, does the provider recognise a repeat of the same request, and by which key?

    experiment Send the same test-mode authorise twice with one idempotency key and once without; count the charges in the sandbox dashboard.

  2. ? Emails.

    becomes If the email service rejects a send, do we find out synchronously, by webhook, or never?

    experiment Send to an address the sandbox documents as bouncing and record every signal that comes back and when.

  3. ? The shipping API.

    becomes How stale may a quote be before the carrier will not honour it, and what is the allowed request rate?

    experiment Read the contract for both numbers, then request quotes in a burst against the sandbox until it refuses.

ShapeStore exampleYou must decideHands off to
Wait-for callauthorise payment; carrier quotetimeout, what the user sees on timeout, whether it sits in a transactionWhich Dependency Must Answer Before the User Can Be Told Anything?, External Systems Fail
Fire-and-forgetconfirmation emailretry policy, how you notice it never sent, whether the user is told "will be sent"Partial Failure
Inbound messagesettlement webhook; tracking updatesignature check, duplicate handling, out-of-order handlingDuplicate Requests
Stored objectproduct imagewhat the page shows when the object is missing; who can writeWhat Must Persist

Is this box inside or outside?

The decision is small and it is worth making explicitly for every box, because the reflex sorts by "did we write it" and the move sorts by "can we fix it". The options below are the cases that come up; the middle one is the one people forget.

Which side of the line does this component sit on?

Inside: we can change, deploy and observe it

when our services, our database, our workers, our admin UI — anything a deploy tonight would fix

cost we own its failures completely; there is nobody to wait on

Outside but ours: another team's service, a managed database

when someone in the organisation can fix it, but not us, and not on our schedule

cost treat the crossing like an external one — contract, timeout, plan — while being able to pick up the phone

Outside: a provider we call under a contract

when payments, email, storage, shipping, model APIs — we can only call, retry and wait

cost their changelog is our backlog; their incident is our incident; their rate limit is our capacity

Outside and uncontracted: the customer and their browser

when always

cost requests arrive twice, never, or after the tab was closed; nothing can be assumed to complete

How to do it

Most important first.

  • List every box on your sketch and ask, for each, "can I redeploy this?" The ones where the answer is no go outside the line — and so does the customer.
  • For each outside box, write its contract in a sentence: "we POST a charge and get an id back within a timeout; they POST us a webhook when it settles; the payload shape is in their changelog, not ours."
  • Label each crossing arrow with its shape: wait-for, fire-and-forget, inbound message, stored object. A crossing without a label is a failure plan not yet written.
  • Mark which crossings sit inside a database transaction or a user-facing request. Those are the ones that turn a slow provider into a slow store (Which Dependency Must Answer Before the User Can Be Told Anything?).
  • Keep the boundary drawing next to the capability decomposition; when a new feature arrives, ask first whether it adds a crossing.

Worked on a concrete problem

The move has to produce something. This is what it produced.

  • The store, redrawn. Inside the line: web app, API, order service, database, background worker, the admin UI. Outside: payment provider (wait-for on authorise; inbound webhook on settle), email service (fire-and-forget from the worker), object storage (stored objects, plus a signed URL the browser uses directly), shipping carrier (wait-for on quote; inbound status updates), the customer's browser (inbound requests we do not control the timing of).
  • "What if payments is down?" now reads as: the authorise crossing times out or errors; the order exists in a pending state; the customer is told the payment did not go through and can retry; the webhook crossing may still deliver later, so a late success must reconcile against the pending order rather than create a second one. Each clause is a design decision that was invisible when payments was an internal-looking box (What If Payment Fails?).
  • The shipping quote on every cart render was a wait-for crossing inside a user-facing request, to a metered API. Redrawn, it becomes a cached quote refreshed by the worker, and the cart page has no outside crossings at all — which is the property that makes it reliably fast.
  • The AI assistant case, same move: the model provider is outside (wait-for, slow, metered, rate-limited), the company's document store may be inside or outside depending on who runs it, and the user's question is an inbound request. The diagram changes which of those the assistant can afford to call synchronously.

How you know it worked

What now exists that did not before, and what question you can now ask.

  • The diagram has a visible line, every outside box has a different shape, and every arrow that crosses the line carries a shape label.
  • For each outside system you can say its contract in one sentence and name the changelog or status page you would read when it changes.
  • "What if X is down?" is a question you can answer by pointing at one arrow.

The questions you can now ask

The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.

Next questions
  • ?Which of these boxes could I redeploy tonight, and which could I only wait on?
  • ?For each thing outside the line, what exactly do I send it, what do I get back, and who decides when that changes?
  • ?Which crossings does the user wait on, and which of those sit inside a transaction?
  • ?Does this new feature add a crossing — and if so, what shape is it?

What can go wrong

How the move itself fails
  • The line is drawn around the code you happen to have written, so a database you operate is "outside" and treated with the same suspicion as the provider. The test is control, not ownership of the source: you can restart your database.
  • Everything outside is wrapped in the same generic adapter with the same retry policy. A fire-and-forget email and a wait-for payment authorisation need different failure plans, and one abstraction for both hides the difference.
  • The boundary is drawn once and the diagram is filed. New features add crossings — a tax service, a fraud check, an analytics pixel — and each one arrives as an internal-looking box again.
  • The customer's browser is left inside the line. It is the least controllable component in the system, and designs that assume it will finish what it started produce the double-submit and abandoned-checkout bugs.
What the move costs
  • Treating a crossing properly — adapter, timeout, failure plan — is more code than a direct call, and for a prototype that only ever runs against the sandbox it is code with no observable benefit until the first outage.
  • Pushing crossings out of user-facing requests into workers makes the store faster and adds asynchrony: the confirmation email now "will be sent" rather than "was sent", and the product copy has to say so.
  • A strict boundary can turn into an anti-corruption layer for every provider, which is the right structure for a payments system and an over-investment for a weekend project that uploads images.
Misreads
  • "Outside means untrusted, so validate everything from the provider as hostile." Verify signatures on webhooks, yes; but the provider is a party you have a contract with, and the design question is availability and change, not malice. Security boundaries and system boundaries overlap and are not the same line.
  • "If it is outside, we should build it ourselves to bring it inside." Building payment processing to avoid depending on a provider replaces a well-understood external failure mode with an internal one you have never operated. The line says what to plan for, not what to insource (The Build-vs-Buy Questions).
  • "The database is outside because it is a separate process." A separate process you run, observe and restart is inside. The line is about control, and the point of drawing it is the things you cannot fix by deploying.

Where this applies

Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.

  • GENERALEvery system has a boundary and things beyond it; the test "can I fix it or only wait?" applies to a store, a data pipeline calling a vendor API, an ML service calling a model provider, or a CLI calling a package registry.
  • TEAM-SPECIFICOn a solo project everything you wrote is inside and everything else is outside. In a large organisation another team's service is outside your line too — you cannot redeploy it — even though it is "internal", and the same contract-and-failure-plan treatment applies to it.
  • ILLUSTRATIVEThe store's boundary and the list of crossings are one plausible drawing; a real store has more outside dependencies (tax, fraud, analytics) and the diagram is deliberately kept to the ones the running example already uses.

Where the depth lives

This domain asks the question and hands the answer off by name.