Engineering practices
Part 6 of 6 · Interview Debugging & Implementation — Reproduce, Trace, Fix, ShipLow-Level Design Under Time — Interfaces, State & Tradeoffs
In-interview low-level design for one component: interfaces, invariants, where state lives, and failure. A request-scoped log correlator, with a payment state handler as the same moves.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
What is in scope for this low-level design?
Answer
One module. Types, invariants, state, and failure. Not the whole company's services.
L2
What types do you name for the correlator?
Answer
A validated correlation id, a context, a binder, a log adapter, and middleware.
L3
What are the invariants?
Answer
At most one bound context per task. Outbound headers use that id. clear runs in finally. The id is not personal data.
L4
Implicit context or an explicit parameter?
Answer
Implicit for the logger. Explicit headers at the boundary. Thread-local alone is wrong for async.
L5
What if the inbound header is missing or invalid?
Answer
Say which. A common API choice is to mint an id and log that it was generated, rather than 400 every client. Invalid junk can 400 if you documented that.
L6
When do you add a state machine?
Answer
When a lifecycle has transitions you must forbid. A switch on known headers does not need a strategy hierarchy.
L7
Where does concurrency bite?
Answer
Lost context on a thread pool, a shared mutable dict, and a double-bind in a nested task. Native request context plus clear in finally is the fix.
Failure modes
Id minted mid-request
The outbound call invents a new id. Logs and the downstream trace split into two stories.
Context leaked on a worker
clear never runs. The next job on that worker inherits the previous request id.
Logging throws
A missing context crashes the request. The request should log request_id=unknown and continue.
Pattern soup
Strategy, decorator, and a full statechart appear before bind and clear exist.
Misconceptions
Low-level design means every Gang of Four pattern you can name.
A pattern earns a place when a simpler function cannot forbid the illegal transition. Otherwise it is a clock tax.
Passing the id to every function is always cleaner.
It is clearer for business logic and noisy for every log line. Hybrid is allowed. Say so.
A system sketch and a class sketch are the same interview.
Services and stores are the wide sketch. This page is one component. If they ask for the wide one, draw one context diagram and come back.
Interviewer traps
Design log shipping, sampling, and a redaction pipeline.
Name them as follow-ups. The component binds an id and injects fields.
Open a pattern catalog or a company-wide architecture.
Stay on this component. There is no second series hiding behind the word design.
Design scenario
Same prompt for every reader.
Requirements
Types, invariants, where state lives, failure for a missing header, and tests for leak, invalid id, and outbound header equality.
Traffic / scale
One request at a time in the sketch. Mention pooled workers only as the leak.
Latency
Binding context is not the latency story. Do not profile it here.
Consistency
The outbound header equals the bound id. A second id is a bug.
Availability
A logging failure must not fail the checkout request.
Failure assumptions
- Workers are reused.
- Some clients omit the header.
- Nested tasks can bind twice if you are careless.
Constraints
- Do not design the log backend.
- Do not draw every service in the company.
Prompt
Design the classes for a request-scoped log correlator used by checkout. If you prefer payments, design the state handler instead, with the same discipline.
API
What does bind reject, and what does a missing header do?
Data
Where does the context live inside the process, and what crosses the wire?
Architecture
Which transitions are illegal, and which pattern did you refuse?
What you draw when they say design
Prefer
One component and its illegal edges
Context, binder, adapter, middleware. Unbound to bound to clear. Logging with no context degrades instead of throwing.
- Invariants fit in three sentences.
- A function extracts known headers. A strategy tree does not.
- Tests cover leak, invalid id, and the outbound header.
Alternative
The company, plus every pattern
Boxes for every service, then a strategy per header format, before bind exists.
- The clock dies in the diagram.
- Nothing is typed, so nothing can be wrong.
- The request id leak on a worker never gets named.
Order of the sketch
If you cannot say the invariant, you are not ready for a pattern name.
- 1
Name the noun
A correlation context, or a payment state. One noun. - 2
Write types and invariants
What is stored, what is rejected, what must be unique per request. - 3
Place the state
Implicit context in-process. Headers across the network. A row if the noun is a payment. - 4
Name failure
Missing header, invalid id, unbound log, leaked worker. - 5
Add a pattern only if it forbids something
A transition map forbids log-while-unbound. A strategy hierarchy for two headers usually does not.
One component versus a system sketch
| Wide sketch | This interview | |
|---|---|---|
| Scope | Services, stores, queues, trust boundaries | One module |
| Artifacts | Boxes across the system | Types, state, and the sequence inside the box |
| Failure | A region, a dependency budget | A null context, a missing header, a concurrent mutate |
| Trap | Designing the company | Designing twenty patterns you will not code |
If they ask for the wide sketch, draw one context diagram, then return to the correlator or the payment handler they are scoring. This cluster does not grow a second series for that diagram.
Request-scoped log correlator
Responsibilities:
- Create or accept a request id at the edge.
- Store it in request scope (
contextvarsorAsyncLocalStorage). - Inject the fields into every log line for this request.
- Propagate the bound id on outbound headers.
- Clear at the end of the request so the next task cannot see it.
Not in this component: log shipping, sampling policy, a redaction engine. Mention them as follow-ups. The query you would run with the id is the signal lesson.
Types
CorrelationId, a string newtype: non-empty, max length, a charset you state.CorrelationContext, holdingrequest_idand an optionaltenant_id.ContextPropagator, withbind,current, andclear.LogAdapter, wrapping the logger and merging context fields.HttpMiddleware, extracting or creating the id on the way in and injecting it on the way out.
Invariants
- At most one bound context per async task or request.
- The outbound header uses the bound id. It never mints a second id mid-request.
clearruns infinally, including on pooled workers.- The id is not personal data.
Where state lives
| Option | Pros | Cons |
|---|---|---|
| Implicit context | Logging stays ergonomic | Hidden coupling, and pools leak if you forget clear |
| Explicit parameter | Data flow is obvious | Noisy signatures on every log call |
| Thread-local only | Simple for a sync server | Wrong for async, and the leak is worse |
Prefer implicit context for the logging adapter. Pass the id explicitly across the process boundary as a header.
Failure
- Missing inbound header: mint an id and log that the source was generated. Say so.
- Invalid header: either 400 or mint-and-warn. APIs that do not want to break old clients often mint. Pick one and keep it.
- Logging with nothing bound: write
request_id=unknown. Do not crash the request because logging failed.
Flow
- 1
1 Request starts unbound
- next2 Bind one valid context
- 2
2 Bind one valid context
- next3 Log with those fields
- 3
3 Log with those fields
- next4 Inject outbound header
- 4
4 Inject outbound header
- next5 Clear in finally
- 5
5 Clear in finally
Lesson map
Low-Level Design Under Time — Interfaces, State & Tradeoffs
In-interview low-level design for one component: interfaces, invariants, where state lives, and failure. A request-scoped log correlator, with a payment state handler as the same moves.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB a["1 Request starts unbound"] b["2 Bind one valid context"] c["3 Log with those fields"] d["4 Inject outbound header"] a -->|1 Request starts unbound| b b -->|2 Bind one valid context| c c -->|3 Log with those fields| d
There is no self-loop on Bound. A second log is the same node, not a transition that has to be drawn. The illegal edge, log while unbound, is a refused transition in the sandbox rather than an arrow that doubles back on itself. That keeps the card a single column.
When a pattern earns its place
A small allowed-transition map helps you talk about illegal moves: log while unbound degrades or throws, and bind-twice is an error you choose explicitly. A strategy hierarchy for header formats is usually a function, extract(headers), with a branch per known header. Pros of the map: nonsense becomes a test. Cons: more types. Choosing the hierarchy burns the clock and ships nothing.
The same moves on a payment
If they want a payment handler instead of the correlator, the noun changes and the rules do not. States might be created, then authorizing, then captured or failed. captured back to authorizing is illegal. Say where the state lives (a row with a version, not a process-local variable) and how a concurrent update loses (compare-and-set on that version). Do not start a second series. Do not turn the sketch into a distributed transaction lecture. The retry contract, if you already built it, stays on the feature lesson.
Sandbox
Bind and clear, then a transition map that rejects log-while-unbound.
ProblemBind r-123, read it back, reject log while unbound, and clear so the next read is empty.
Expectedseen is r-123, illegal is True, after clear the current context is None, and Unbound plus bind becomes Bound.
Edge cases
- An empty id raises.
- An id longer than 128 raises.
- Reset uses the token from set, not a second clear path.
- Test: bound id is visible
seen == 'r-123' - Test: unbound log is illegal
illegal is True - Test: clear drops the context
after is None - Test: bind from unbound is legal
transition('Unbound', 'bind') == 'Bound' - Test: empty id is rejected
empty_rejected is True
Press Run. Snippets must be self-contained — no network, files, or native modules.
ProblemBind r-123, reject log while unbound, and restore the previous slot on clear.
Expectedseen is r-123, illegal is true, after clear current is null, and bind from Unbound returns Bound.
Edge cases
- A second bind saves the previous context and clear restores it.
- An over-long id throws.
- Test: bound id is visible
seen === 'r-123' - Test: unbound log is illegal
illegal === true - Test: clear restores null
after === null - Test: bind is a legal edge
transition('Unbound', 'bind') === 'Bound' - Test: empty id throws
emptyRejected === true
Press Run. Snippets must be self-contained — no network, files, or native modules.
Interview Q&A
Design the classes for this correlator.
Answer
Walk CorrelationContext, the binder, the log adapter, and the middleware. State the invariants. Then the sequence in words: inbound extract or mint, bind, handler logs, outbound header copies the bound id, clear in finally. The id's journey through logs is the signal lesson, not a second design.
Why not pass request id into every function?
Answer
Explicit parameters are clearer for business logic. A logging adapter that required the id on every call becomes noise. Hybrid is the answer: context inside the process, header at the boundary.
How do you test it?
Answer
Two sequential requests must not share an id. An invalid id is rejected. The outbound header equals the bound id. Unbound plus log is the illegal transition. Clear in finally is the leak test.
Where does concurrency bite?
Answer
A thread pool that loses or keeps context, a shared dict mutated without a request scope, and a nested task that binds a second time. Use the platform's request context and clear in finally. Do not invent a lock hierarchy for a single id.
How is this different from a system sketch?
Answer
The wide sketch names services and stores. This sketch names types inside one box. If they insist on the wide sketch, one context diagram, then back to the illegal edges.
When would you switch the example to payments?
Answer
When they say the component is the payment handler. States: created, authorizing, captured or failed. Captured does not return to authorizing. Persistence is a versioned row. The correlator rules about invariants and illegal edges are the same moves.
What pattern do you refuse?
Answer
A strategy per header name, a decorator stack around the logger, and a general statechart library. extract plus the allowed map is enough to test the illegal edge.
What is out of scope even if they keep asking?
Answer
Log shipping, sampling, and a redaction engine. Personal data does not belong in the id. Those are follow-ups. The hub is where you ask which slice they still want scored.
Pitfalls
- Minting a new id on the outbound call.
- Forgetting clear on a pooled worker.
- Crashing the request because the logger had no context.
- Drawing the whole company when the prompt said classes.
- Naming patterns that do not forbid a transition you care about.
Say the five types. Say the three invariants. Say what a missing header does. Say one illegal transition and the test that catches it. Stop. If you reached a pattern name before the invariants, start over.
Go deeper
- OpenTelemetry context propagation and W3C Trace Context for the header this middleware should set.
- contextvars and AsyncLocalStorage for the in-process slot.
- A transition map is enough. A general statechart library is the thing this page refuses under a clock.
The loop returns to the hub.