skip to content

Why is the provisioning port an automated case obtains its records through worth designing before the implementation behind it?

level: seniorimportance: should knowfreq 46%

answer

  1. One narrow way in for records
  2. Cases depend on the request, not the mechanism
  3. Same request, several ways to satisfy it
  4. Domain vocabulary, never store shapes
  5. Second implementation should be a day's work

basics

~20 s

The port is the contract every case is written against; the mechanism behind it is replaceable. Design the request vocabulary first and cases survive when record creation moves from direct writes into the store to the product's own creation interface.

solid answer

~50 s

A **provisioning port** is a narrow interface a case calls to obtain the records it needs — *an account on a paid plan whose balance is overdue* — and nothing more. Cases depend only on that vocabulary, so the way records actually appear can change without touching them: an early suite may write straight into the record store for speed, a later one may drive the product's own creation interface so the records pass real validation, a third may claim a pre-built record from a pool. Each is one implementation behind the same port. Designing the port first also forces the request vocabulary into the language of the domain rather than of the store, which is what makes cases readable. The mechanism is a speed-versus-fidelity decision you will revisit; the port is what hundreds of cases are written against, and changing *that* later is the expensive edit.

code

pseudocode · 23 lines
pseudocode
interface ProvisioningPort:
    account(plan, standing) -> Handle
    order(owner, status)    -> Handle

# implementation A: write records straight into the store
class DirectProvisioning implements ProvisioningPort:
    account(plan, standing):
        row = store.insert_account(plan = plan, standing = standing)
        return Handle(id = row.id, credentials = row.secret)

# implementation B: drive the product's own creation interface
class ThroughTheProduct implements ProvisioningPort:
    account(plan, standing):
        made = api.create_account(plan = plan)
        if standing == "overdue":
            api.add_failed_charge(made.id)
        return Handle(id = made.id, credentials = made.secret)

# the case names neither one
case "overdue account sees the payment banner":
    account = provisioning.account(plan = "paid", standing = "overdue")
    app.sign_in(account.credentials)
    assert app.banner().says("payment overdue")

go deeper

for a junior

Be ready to say where the records your case needs come from, and to point at the one call that creates them rather than at setup code scattered through the case.

for a middle

Explain what a provisioning port is — a narrow interface the case calls for the records it needs, with the creation mechanism hidden behind it — and name two different mechanisms that could sit behind the same request.

for a senior

Show that you have swapped a mechanism behind a port on a real suite and can say what broke and why. An interviewer expects you to weigh speed against fidelity for a specific suite rather than declaring one mechanism correct.

for a principal

Own the argument that the request vocabulary is the long-lived asset: hundreds of cases are written against it, so its shape is a design decision, while the mechanism behind it is a cost decision you expect to revisit.

A suite's cases cannot start until something has put the product into the state they need. The **provisioning port** is the single interface through which that happens: a small, named vocabulary of requests that cases call, with the machinery that satisfies them hidden behind it. Getting the port right is a different job from getting the machinery right, and it is the one that has to be done first. ## What a provisioning port is The port has exactly one job: hand a case the records it needs to begin, and hand back handles to what it created. It is not a general-purpose client for the product, not a data access layer cases can query through, and not a place assertions live. Its vocabulary is small and stated in domain terms — an account, an order, a subscription — each parameterised by the states cases actually distinguish. What makes it a *port* rather than a helper is that it has implementations, plural, and the case cannot tell which one it is talking to. ## The mechanisms that can sit behind it | Mechanism | Cost per record | Fidelity | What it couples the suite to | | --- | --- | --- | --- | | Writing records straight into the store | Lowest | Lowest — skips the product's own validation, derived fields and side effects | The store's schema | | Driving the product's own creation interface | A round trip per record | Highest — the record is one a user could have produced | The creation interface's contract | | Claiming a pre-built record from a pool | Lowest at case time | Whatever filled the pool | Pool refill, and the record's condition when claimed | None of these is universally right. A suite whose runtime is dominated by setup moves toward writes or a pool; a suite whose failures keep turning out to be records the product would never have produced moves toward the creation interface. Most mature suites end up mixed: the bulk through the fast path, a handful of high-fidelity cases through the slow one. ## Why the seam outranks the mechanism Three properties make the port the more consequential decision: - **It has far more dependants.** One mechanism is written once; the vocabulary is called from every case in the suite. The blast radius of changing a request signature is the whole suite; the blast radius of changing how that request is satisfied is one file. - **You will change the mechanism at least once.** Fidelity complaints, runtime pressure, or a store the suite loses direct access to all force the swap. A suite that anticipated it pays a day; a suite that did not pays a migration. - **The vocabulary shapes what cases can say.** If the only request the port offers is *create an empty account*, every case that needs an interesting state builds it inline, and the interesting states are the ones worth naming once and reusing. ## Designing the request vocabulary 1. **Name domain entities and domain states, never store shapes.** A request that names a table and a column value is a direct-write mechanism wearing a port's name; the first case that reads it now depends on that mechanism. 2. **Parameterise only the states cases actually distinguish.** Every parameter is a promise the port must keep across all its implementations. A parameter used by exactly one case usually belongs in that case, applied to a plainly provisioned record. 3. **Return handles, not raw rows.** A handle names what was created and lets the case refer to it without knowing how it was stored. 4. **Keep checking out of it.** A port that asserts the record it made is correct has become a test, and its failures will be reported as the calling case's failures. 5. **Keep it narrow enough to reimplement.** The honest test of a port is whether a second implementation is a day's work. If it would take a week, the port has absorbed responsibilities that are not provisioning. ## How it fails in practice - **No seam at all.** Every case creates records its own way. There is nothing to swap, and the migration everyone eventually needs is a suite-wide rewrite. - **One method per case.** The vocabulary grew with the suite instead of with the domain, so the port is a second copy of the suite and every new case adds to it. - **A leaky name.** A request whose name describes the mechanism rather than the outcome teaches every reader to depend on the mechanism. - **The port that grew.** Assertions, target selection and cleanup policy all drift into it because it is the one place every case already calls. Each addition makes the second implementation harder, which is the measurable symptom. The rule of thumb is blunt: decide what a case is allowed to ask for before deciding how you will answer it. The first decision is expensive to reverse; the second one you were always going to revisit.

  • A team swaps the mechanism behind the port from direct writes to the product's own creation interface, and a third of the suite starts failing. What does that tell you?
    That those cases were relying on records the product itself would never produce — states its validation forbids, or derived values the creation path fills differently. The failures are usually real information rather than noise: the suite had been asserting on shapes the product cannot reach. Fix the cases, or accept that a few states are only reachable by direct writes and mark those cases as deliberately lower fidelity.
  • How wide should the port be — one request per case, or a small vocabulary?
    A small vocabulary. One request per case turns the port into a second copy of the suite, and every new case adds a method nobody else will ever call. Aim for a handful of domain entities with parameters for the states that genuinely differ, and let cases compose them. A parameter used by exactly one case is usually a detail that case should apply itself.
  • What is the cheapest signal that a provisioning port has stopped being a port?
    How hard a second implementation would be. If writing one means reproducing target selection, cleanup policy and a set of assertions that crept in, the port has absorbed work that is not provisioning. Sketching the alternative implementation periodically is a quick, honest audit — it costs an hour and it names exactly which responsibilities have drifted in.

Ordering a coffee names what you want, not which machine makes it. The counter is the contract; the machine behind it can be replaced overnight without anyone relearning how to order.

saying these in an interview costs you the question

  • Calling the port pointless indirection over a direct write
  • One provisioning request per case, growing with the suite
  • Exposing store table and column names through the port
  • Insisting all provisioning must go through the product's interface
  • Letting each case invent its own way to create records
  • Adding assertions to the provisioning path for convenience