Skip to content
QSWEQB

Architecture and tech leadership fundamentals

Architecture as a practice rather than a diagram: which decisions are expensive to reverse, how to elicit an attribute nobody stated, where a boundary belongs, and how a migration is sequenced. Fifty-six items, thirteen worked through with an ADR, a table or a diagram.

56 questions

Go deeper on Architecture & Tech Leadership

What an architect actually does

What is architecture, if it is not the diagram?

The set of decisions that are expensive to reverse. Which language a single handler is written in is not architecture, because you can rewrite it on a Tuesday; how services are partitioned, what the data model of the core entity is, whether communication is synchronous, and which vendor holds your identity are architecture, because undoing any of them means coordinated change across teams and a data migration. The diagram is an output, and often a stale one. The useful test on any decision is to ask what it would cost to change your mind in eighteen months, and to spend design effort in proportion to that answer rather than in proportion to how interesting the decision is.

What does an architect do that a senior engineer does not?

Work at the seams. A senior engineer is accountable for a component being correct, fast and maintainable; an architect is accountable for the properties that no single component owns — how the system behaves when a dependency is slow, whether two teams can deploy independently, whether the whole thing still makes sense after the next three features. That means most of the work is elicitation, negotiation and writing rather than code. The failure mode of the role is producing decisions nobody implements, which is why the good version stays close enough to the code to be corrected by it.

Show me an ADR written out in full.

Candidates describe ADRs in the abstract constantly and rarely produce one, so the shape is worth having ready.

ADR 014 — Use an outbox table for order events

Status:   Accepted
Date:     2026-04-18
Deciders: Payments team, platform architecture
Supersedes: none

Context
  Order state changes must reach fulfilment and analytics. Today the service
  writes the order, commits, then publishes to the broker. Over the last
  quarter we have found 37 orders with no corresponding event, all after
  instance restarts. Fulfilment reconciles nightly, so the business impact is
  a delay of up to 20 hours on affected orders.

Decision
  Write the event into an `order_outbox` table in the same transaction as the
  order. A relay process polls the table, publishes, and marks rows sent.
  Consumers deduplicate on the event id.

Consequences
  + One durable decision point; the lost-event class of bug is eliminated.
  + No new infrastructure; the table lives in the existing database.
  - Delivery becomes at-least-once, so every consumer must be idempotent.
    Fulfilment already is; analytics needs a change, tracked as PAY-2210.
  - The outbox table needs pruning; added to the existing retention job.
  - The relay is a new operational component with its own lag metric.

Alternatives considered
  Change data capture on the orders table — rejected for now: it removes the
  relay but couples consumers to our schema, and we would still need an
  event-shape translation layer.
  Publish first, then write — rejected: produces the mirror bug, an event for
  an order that does not exist.

Four things make this an ADR rather than a design document. The context contains a number, so a reader in two years can tell whether the pressure that forced the decision still exists. The consequences include the ones we dislike, because an ADR listing only benefits is advocacy and will be distrusted the first time a cost surfaces. The rejected alternatives are recorded with reasons, which is what stops the same argument being had quarterly by whoever joins next. And it has a status, so superseding it later is an act of authorship rather than a contradiction.

What does not belong: the class diagram, the sprint plan, or the rationale for anything cheap to reverse. An ADR is for the decisions you will be asked to justify after the people who made them have left.

What is the difference between a functional and a quality-attribute requirement?

A functional requirement says what the system does — a user can cancel an order. A quality-attribute requirement says how well it must do it — the cancellation is reflected in the warehouse within thirty seconds for the 99th percentile, and survives one availability-zone failure. Functional requirements are usually written down because someone wanted the feature; quality attributes are usually assumed, and they are what actually drives architecture. Two systems with identical feature lists and different availability and latency targets are different architectures. That asymmetry is why an architect's first job on any brief is to extract the second list, which nobody has written.

Why is an architect who does not read code a warning sign?

Because the decisions are validated in the code, and without that feedback loop the architecture drifts into fiction. The specific failure is prescribing something the codebase cannot absorb — a boundary that cuts through a class every feature touches, a caching layer whose invalidation the existing write paths make impossible — and never learning, because the team quietly works around it and the diagram still says otherwise. Reading code is also how you find out what the real architecture is, which is frequently not the documented one. The point is not that the architect should be the fastest coder; it is that decisions need to be falsifiable by contact with the system.

What makes a decision reversible, and why does that change the process?

A decision is reversible when undoing it touches one deployable unit and no persisted data, and irreversible when it has been written into a schema, a public API, an external contract or another team's code. The consequence is that process should scale with reversibility rather than with visibility. Choosing an internal library needs a conversation; choosing the identity provider or the partition key of your largest table needs a written decision, alternatives and a named owner. Teams get this backwards constantly, running heavyweight review over easily reversed choices while irreversible ones happen in a pull request nobody senior read.

Who owns an architectural decision?

Whoever will operate the consequence, with the architect accountable for the decision having been made properly rather than for making it. That distinction is what separates an architecture function that scales from a bottleneck: the architect's deliverable is often the framing — here are the three options, here is what each costs, here is the attribute we are optimising — and the owning team chooses. Decisions that cross team boundaries or bind the organisation are the exception and are genuinely the architect's to make. Ownership must be recorded, because an unowned decision decays into an unwritten convention and then into an argument.

Quality attributes and trade-offs

Show me a quality-attribute scenario in the six-part form.

"It should be highly available" is not testable. The six-part scenario is the standard way of turning an aspiration into something you can pass or fail.

Source        A logged-in customer
Stimulus      Submits a checkout for a basket of up to 40 items
Environment   Normal operation, weekday peak, one availability zone lost
Artefact      The checkout service and its payment dependency
Response      The order is accepted and the customer sees a confirmation
Measure       p99 under 1.5s end to end; zero accepted orders lost;
              degraded mode allowed to omit loyalty-points display

Same artefact, a second scenario for the failure case:

Source        The payment provider
Stimulus      Begins returning 503 for all requests
Environment   Normal operation, no other faults
Artefact      Checkout
Response      Checkout stays available and queues authorisation, or fails
              fast with a retryable message — chosen, not accidental
Measure       Detected within 30s; no customer waits longer than 3s for
              the answer; backlog drains within 15 min of recovery

Read the two together, because the pairing is what makes the technique work. The first scenario constrains the happy path and the second names a specific fault, and the second is where the architecture is actually decided — everything about timeouts, circuit breakers, queues and degraded modes falls out of it.

The environment row is the one candidates omit and the one that carries the cost. A p99 of 1.5 seconds in normal operation is an ordinary engineering target; the same number with a zone missing implies capacity headroom you must pay for all year. Making the environment explicit is what stops a target being agreed cheaply and discovered expensively.

The measure row has to be a number someone can instrument, and it is worth including what may be given up. "Degraded mode may omit loyalty points" is a product decision recorded in an engineering artefact, which is exactly where it belongs, because otherwise an engineer makes it at three in the morning.

Write four to six of these for a system and you have its architecture drivers. Write none and every subsequent argument about design is a matter of taste.

How do you elicit an NFR a stakeholder has not articulated?

By asking about consequences rather than about requirements, because stakeholders know their business and do not know the vocabulary. Instead of "what availability do you need", ask what happens if this is down for an hour on a Friday afternoon, who calls whom, and whether anyone can do the job on paper meanwhile. Instead of "what latency", ask what the user is doing while they wait and what they do if it takes ten seconds. Ask for the worst incident they remember. Then translate the answers into numbers, read them back, and let the stakeholder argue with the number rather than invent it.

Which quality attributes are genuinely in tension?

Availability against consistency, because staying answerable during a partition means answering from data that may be stale. Security against usability, because every additional verification step is friction, and the sharpest version is that the most secure system is the one nobody can use. Performance against maintainability, because the fast version is usually the one that inlines, denormalises and special-cases. Time to market against nearly everything. Naming the tension explicitly is the point: an architecture is a chosen position on several of these axes, and a stakeholder who is told they can have all of them has been lied to by omission.

Show me a trade-off table for three options scored against attributes.

Scoring is only useful if the weights come from the business first, so the table below has them fixed before any option was scored.

Decision: how the search index stays in step with the orders database
Weights agreed with product: consistency 5, latency 3, ops cost 4, effort 2
Scores 1-5, higher is better. Weighted total in the last column.

option                    consistency  latency  ops cost  effort   total
------------------------  -----------  -------  --------  ------  ------
A  dual write from app        2 (10)    5 (15)    5 (20)   5 (10)     55
B  outbox + relay             5 (25)    4 (12)    3 (12)   3  (6)     55
C  change data capture        5 (25)    4 (12)    2  (8)   2  (4)     49

Tie between A and B on the arithmetic. Consistency is the attribute the
incident log says we keep failing, and A scores 2 on it because a crash
between the two writes silently drops an index update. Chose B.

The tie is deliberate, because it shows what the table is for. It is not a calculator that produces the answer; it is a device for making the disagreement specific. Two people who score the same option differently now have an argument about a cell rather than about architecture in general, and that argument finishes.

The order of operations matters more than the numbers. Agree the attributes and weights before scoring, or the weights get retro-fitted to whichever option somebody already preferred — which is the most common way this technique is abused, and it is visible in the artefact because the weights will happen to favour exactly one column.

Two honest limitations to state before an interviewer does. The scores are judgement dressed as data, so the table should record who scored and against what evidence. And a weighted total cannot represent a veto: if an option fails a hard constraint — a regulator requires an audit trail, and A cannot produce one — it is eliminated rather than scored, because a sufficiently high score elsewhere must not be allowed to buy the constraint back.

What is a fitness function?

An automated check that a quality attribute still holds, run in the pipeline alongside the tests. The idea is that architecture stated only in documents decays silently, whereas architecture stated as a failing build does not: if the domain layer must not depend on the web framework, write a test that fails when it does. They range from dependency rules and bundle-size budgets to a load test asserting a latency percentile and a chaos experiment asserting recovery time. The cost is that each one is a maintained artefact and a flaky one gets disabled, so a small set of enforced checks beats a large set of aspirational ones.

Show me a fitness function expressed as an executable check.

The value is that the rule stops being a convention someone reminds people about in review.

// Architecture rule as a test: the domain must not know about the framework
// or about persistence. Fails the build, not the code review.
@AnalyzeClasses(packages = "com.acme.orders")
class ArchitectureRules {

    @ArchTest
    static final ArchRule domain_depends_on_nothing_outward =
        noClasses().that().resideInAPackage("..domain..")
            .should().dependOnClassesThat()
            .resideInAnyPackage("..web..", "..persistence..",
                                "org.springframework..", "javax.persistence..");

    @ArchTest
    static final ArchRule no_cycles_between_modules =
        slices().matching("com.acme.(*)..").should().beFreeOfCycles();
}
Non-code attributes take the same shape, as pipeline gates:

  latency      k6 run asserts p95 < 400ms on the checkout journey,
               fails the stage above 400ms
  bundle size  CI fails if the initial JS bundle exceeds 250 KB gzipped
  recovery     nightly experiment kills one instance and asserts the
               error rate returns to baseline within 60s
  licences     build fails on any new GPL transitive dependency

The dependency rule is the one worth writing first, because it is the rule most often stated and least often true. Hexagonal, layered and clean architectures all reduce to a constraint on which direction imports may point, and that constraint is mechanically checkable — so leaving it to discipline is a choice to have it violated within two quarters by someone who joined last month and read the code rather than the wiki.

The second block is the part candidates miss. A fitness function is not necessarily a unit test; it is any automated assertion about a system property, including a load test threshold, a cost budget or a chaos experiment. What makes it a fitness function is that it runs without being asked and that failing it blocks something.

The discipline is to keep the set small and the failures real. A rule that fires spuriously gets an exclusion added, then another, and within a year the file documents the violations rather than preventing them. Better three rules that genuinely block than thirty with an allowlist.

Why is "it must be scalable" not a requirement?

Because it names no quantity, no dimension and no cost ceiling, so it cannot be designed for or verified. Scalable along which axis — requests, data volume, tenants, concurrent editors of one document? To what number, by when, and at what unit economics? A system that handles ten times the traffic on ten times the hardware has scaled and may still be a commercial failure. The productive move is to convert it into a scenario with a measure, and to ask what the growth curve actually is, because the honest answer is often that current volume times three would be comfortable and nothing beyond that is funded.

How do you prioritise attributes when the business wants all of them?

By forcing a ranking against a scarce resource rather than asking for one in the abstract, because everything is important when nothing is being traded. Present two options with real consequences — this design gives you the latency target and costs an extra forty thousand a year; this one is cheaper and puts p99 at 900 milliseconds — and the ranking emerges. Where a genuine tie survives, ask which failure the business would rather explain to a customer. Record the result as an ordered list with the reasoning, so the next decision inherits it and you are not re-running the negotiation per design.

Boundaries and decomposition

How do you decide where a service boundary goes?

Four tests, applied together. Bounded context: does this cluster of concepts have its own vocabulary and its own definition of the shared nouns? Transaction boundary: is there an invariant that must hold atomically, because a boundary drawn through one converts a database constraint into a distributed saga. Team ownership: can one team own it end to end, since a service two teams both change is a coordination cost with extra network hops. Rate of change: things that change together belong together. When the four disagree, the transaction boundary and team ownership usually win, because violating either produces daily pain rather than conceptual untidiness.

What is a bounded context?

The scope within which a term has one consistent meaning. "Customer" in sales is a lead with a pipeline stage; in billing it is a legal entity with a tax identifier; in support it is whoever is on the phone. A bounded context is the boundary within which one model of customer holds, and the point is that trying to build a single canonical model spanning all three produces an entity with forty nullable columns that satisfies nobody. Contexts communicate through explicit translation instead, which is why the pattern comes paired with context mapping and anti-corruption layers rather than shared tables.

Show me a boundary drawn wrongly and then by bounded context.

The wrong split is usually by entity or by technical layer, and it looks tidy until you trace one request through it.

flowchart LR
    W1[Checkout request] --> W2[Order service<br/>owns orders table]
    W2 --> W3[Customer service<br/>reads and writes orders]
    W2 --> W4[Pricing service<br/>reads orders table directly]
    W3 --> W2
    R1[Checkout request] --> R2[Ordering context<br/>owns the order lifecycle]
    R2 -. OrderPlaced .-> R3[Fulfilment context<br/>owns shipments]
    R2 -. OrderPlaced .-> R4[Billing context<br/>owns invoices]

The top row splits by noun. Every service is named after a table, so no service can complete a use case alone: checkout needs three synchronous calls, one of them circular, and two services read the same table, which means the schema is a shared public interface that neither owns. Availability multiplies, a change to the orders table requires three coordinated deploys, and the network hops bought nothing.

The bottom row splits by what the business does. Ordering owns the lifecycle of an order and completes checkout by itself, then publishes a fact. Fulfilment and Billing react. Each context has its own store and its own notion of an order — Billing's has a tax treatment, Fulfilment's has a weight — and neither needs Ordering's schema.

The detail that decides whether this works is the invariant. If the business rule is that an order cannot be accepted without a successful payment authorisation, then payment authorisation is inside the checkout transaction and no diagram makes that go away; you either keep it in the same boundary or you accept a saga with a visible pending state and a compensation. Candidates draw the second picture and then quietly assume atomicity across it, and that is the follow-up question every time.

The heuristic that generalises: if completing one ordinary use case requires synchronous calls to three services you also own, the boundaries are wrong. Draw them again around the use case rather than around the data.

What is a distributed monolith?

A system with the deployment topology of microservices and the coupling of a monolith: services that cannot be released independently, share a database, and must be changed together for any meaningful feature. It is strictly worse than the monolith it replaced, because you now have the coordination cost plus network partitions, distributed debugging and duplicated infrastructure, and none of the independence you paid for. It arises from splitting by technical layer or by entity rather than by capability, and from leaving a shared schema in place because migrating the data was the hard part.

Show me a distributed monolith diagnosed from symptoms.

Nobody sets out to build one, so the diagnosis is made from operational evidence rather than from the diagram.

symptom                                        what it proves
---------------------------------------------  ------------------------------
A feature needs a coordinated release of       the boundary does not match
four services, in a specific order             the unit of change

Two or more services write the same tables     the schema is the real
                                               interface and nobody owns it

Every service must be up for the main          you bought network hops and
journey to work at all                         no independence

Local development requires running eleven      no service has a testable
containers                                     contract

A shared library must be version-bumped        the library is a monolith with
everywhere for any change                      a package manager in front

Incidents always require three teams on        ownership is not aligned to
the call                                       the failure domain

Four of the six are release-coupling symptoms, which is the actual definition — independent deployability is the property microservices exist to buy, so its absence means the split has cost you everything and returned nothing.

The two to check first are the shared tables and the ordered release, because they are cheap to establish and hard to argue with. Ask for the last four releases and count how many services each touched; ask which services hold credentials to which schema. Both are facts, and both are more reliable than anyone's account of the architecture.

The fix is rarely to add more services. It is to consolidate: merge the services that always change together back into one deployable, then re-split along the seam the release history actually shows, giving each new boundary its own store. That sequence — merge, then split by evidence — is the answer interviewers are listening for, because the instinctive answer is to add an event bus, which leaves the coupling in place and makes it asynchronous and harder to see.

Which kinds of coupling actually hurt?

Not all of them, and the distinction is the whole skill. Deployment coupling — needing to release two things together — hurts most, because it removes the benefit of separation while keeping the cost. Data coupling through a shared schema hurts nearly as much, since it makes every migration a cross-team negotiation. Temporal coupling, needing another service up right now, costs availability and is sometimes the correct price for consistency. Domain coupling is unavoidable and healthy: fulfilment genuinely depends on the concept of an order. The trap is treating all coupling as bad and building indirection that hides a real dependency instead of removing it.

Show me the coupling types and which ones to accept.

Ranked by what each costs in practice rather than by taxonomy.

type          means                              cost           accept?
------------  ---------------------------------  -------------  ---------------
deployment    B must ship with A                 highest        no, fix it
schema/data   both read or write one table        very high      no, own your data
temporal      B must be up when A is called       availability   sometimes, for
                                                                 a real invariant
functional    A knows B's business rules          brittle        no, publish
                                                                 events instead
semantic      shared meaning of a term            unavoidable    yes, and map it
                                                                 at the edges
contract      A depends on B's published API      designed       yes, that is the
                                                                 point

The top two are the ones to remove, and they are removable: give each service its own store, replace the shared table with an API or an event, and the ordered release goes away with it. Everything below is a design choice rather than a defect.

Temporal coupling is the interesting row, because the instinct is to eliminate it with a queue and that instinct is often wrong. Making payment authorisation asynchronous does not remove the wait, it relocates it somewhere the user cannot see and adds a correlation problem. Accept temporal coupling where the caller genuinely needs the answer to proceed, and remove it where the work merely has to happen.

Contract coupling is what you are aiming for and candidates sometimes apologise for it. A stable published interface that others depend on is the goal state; the work is versioning it and not breaking it, not pretending the dependency is absent. And semantic coupling is the one that cannot be engineered away — two contexts sharing the word "order" will always need translation, which is exactly what an anti-corruption layer is for.

What is Conway's law, and what is the inverse manoeuvre?

Conway's observation is that a system's structure mirrors the communication structure of the organisation that built it, because interfaces form where conversations are expensive. The inverse manoeuvre is to use that deliberately: decide the architecture you want and then reorganise teams to match it, on the grounds that fighting the organisation chart with a diagram loses. It is why a three-team split will produce three components regardless of your intent, and why a shared component with no owning team decays. The uncomfortable implication for architects is that the highest-leverage architectural act is sometimes a team boundary change, which is not theirs to make alone.

Architectural styles

Monolith, modular monolith or microservices — what is the honest sequencing?

Start with a monolith, keep it modular from the first week, and extract a service only when a specific pressure forces it. The pressures that justify extraction are real: independent scaling of one hot path, a different availability requirement, a compliance boundary, or a team that cannot ship because of contention in a shared codebase. Absent one of those, distribution adds latency, partial failure and operational cost while buying nothing. The reason to keep the monolith modular anyway is that the modules are where the eventual service boundaries come from, and a well-bounded module is a weekend's extraction while a tangled one is a project.

What does a modular monolith need in order to stay modular?

Enforcement, because module boundaries in one codebase are honoured only while someone is watching. That means an explicit dependency rule between modules checked in the build, each module owning its own tables with no cross-module queries or joins, communication through published interfaces or in-process events rather than reaching into another module's internals, and a package or build structure that makes a violation a compile error. Without those, "modular monolith" describes an intention, and eighteen months later the extraction you were preserving optionality for is impossible because everything touches everything.

When is event-driven the right choice over request-response?

When the producer does not need to know who consumes, and the consumer's failure must not fail the producer. Publishing OrderPlaced and letting fulfilment, analytics and loyalty react independently means adding a fourth consumer changes nothing upstream, which is the actual win — extensibility at the boundary rather than performance. Request-response is right when the caller needs the answer to proceed, when the workflow has a single owner who must know it completed, and whenever a clear synchronous contract is easier to reason about than an implied choreography. Most systems want both, which is fine as long as the choice per interaction is deliberate.

What does event-driven architecture actually cost?

Legibility, mainly. No single place describes the workflow, so answering "what happens when an order is placed" requires reading every consumer, and a new engineer cannot follow the flow in a debugger. It also imposes idempotency on every consumer, since delivery is at-least-once; ordering guarantees only within a partition; and a schema-evolution discipline on the event payload, which is now a public contract with unknown consumers. Failures go quiet, because a stuck consumer produces no errors on the producer's dashboard. Choosing it means committing to consumer-lag monitoring and to some form of workflow visibility, or you have traded a legible system for an unobservable one.

What is the difference between layered and hexagonal architecture?

A layered architecture stacks web over service over persistence, with each layer depending downward, so the domain ends up depending on the database — which is why the entity classes carry ORM annotations and the business rules cannot be tested without a schema. Hexagonal architecture, also called ports and adapters, inverts that: the domain defines interfaces it needs and everything external implements them, so all dependencies point inward. The practical payoff is that the domain becomes testable in isolation and infrastructure becomes swappable. The cost is more indirection and more files, which is real and is why it is overkill for a service that is mostly forwarding requests.

Show me a hexagonal architecture's dependency direction.

The whole idea is one rule about which way the arrows point, and the diagram is the rule.

flowchart LR
    H[HTTP adapter] --> IP[Inbound port<br/>use-case interface]
    S[Scheduler adapter] --> IP
    IP --> D[Domain<br/>no framework imports]
    D --> OP[Outbound port<br/>repository interface]
    PG[Postgres adapter] --> OP
    KF[Kafka adapter] --> OP

Every arrow ends closer to the domain, and nothing leaves it. The domain declares an outbound port — an interface it needs, expressed in its own vocabulary, such as OrderRepository — and the Postgres adapter implements it. That is dependency inversion doing the actual work: at runtime the domain calls the database, and at compile time the database depends on the domain.

Two consequences follow immediately. The domain has no framework or driver imports, so its tests need no container and no schema and run in milliseconds, which changes how much of the business logic is genuinely covered. And swapping an adapter — Postgres for DynamoDB, HTTP for a queue consumer — touches no domain code, because the port did not describe the technology.

The mistake that hollows this out is defining the outbound port in the infrastructure's language. A port with findByCriteria returning JPA entities and leaking a Pageable has inverted the import direction and kept the conceptual dependency, so you have the indirection cost and none of the benefit. The port's signature has to be a sentence the domain would say.

The honest limitation is that this pays off in proportion to how much domain logic exists. A service whose job is to validate a payload and write a row does not have a domain worth protecting, and the ports there are ceremony.

What is the difference between orchestration and choreography?

Orchestration puts a coordinator in charge: one component knows the steps, calls each participant and holds the workflow state, so the process is legible, the current position is queryable and compensation is somewhere specific. Choreography has each participant react to events with no central script, so adding a participant needs no change to anyone, at the cost that the workflow exists only as an emergent property of several codebases. The rule of thumb is to orchestrate processes the business cares about as a whole — an order lifecycle someone will ask the status of — and choreograph reactions that are genuinely independent, like sending a receipt or updating an index.

Evolution and migration

What is the strangler fig pattern?

Incremental replacement: put a facade in front of the legacy system, route one capability at a time to new code behind it, and shrink the old system until nothing routes to it and it can be deleted. The name comes from the vine that grows around a tree and eventually stands on its own. Its value is that value ships continuously and every step is individually revertible, so the programme survives a change of priorities — which is the actual reason rewrites fail. Its cost is that two systems run at once, with the data-synchronisation and double-maintenance burden that implies, for as long as the migration takes.

Show me the strangler fig over three phases.

The facade is what makes it possible, because it decouples the routing decision from both implementations.

flowchart LR
    C[Client] --> F[Facade or proxy<br/>routes per capability]
    F --> L[Legacy monolith]
    F --> N[New service<br/>one capability]
    N --> A[Anti-corruption layer]
    A --> LD[Legacy data<br/>still authoritative]
    F --> M[More capabilities moved<br/>new service owns its data]
    L --> X[Legacy retired<br/>facade simplified or removed]

Phase one is the facade alone, routing everything to the legacy system and changing no behaviour. That step feels like no progress and is the most important one, because it is where you discover the undocumented callers, the direct database clients and the batch job nobody owned. Shipping it with zero functional change also proves the routing layer before anything depends on it.

Phase two moves one capability — chosen for low risk and high learning, not high value — and reads legacy data through an anti-corruption layer while the legacy store stays authoritative. Running both paths and comparing outputs before switching traffic is what turns a rewrite into a verified migration.

Phase three is the part programmes skip, and skipping it is how you end up permanently maintaining two systems plus a facade. Ownership of the data has to move so the new service is authoritative, and the legacy path must actually be deleted. A migration with no funded decommissioning step is a migration that adds a system.

Two things to say unprompted. The facade needs to be genuinely dumb — the moment business logic accumulates in it, you have created a third system to migrate. And each phase must be revertible by changing a route, which means the new path cannot have written data the old path cannot read, and that constraint is what governs the data strategy rather than the other way round.

Why do big-bang rewrites fail, and how would you run one if forced?

They fail because the requirement is "everything the old system does", which nobody knows in full, and because the old system keeps changing while you build, so you are chasing a moving target with no shipped value to defend the budget. Eighteen months in, the business asks what it has received and the answer is nothing. If forced — a platform is genuinely end of life, or a licence expires — run it with a hard scope freeze on the legacy system, a shadow-running period comparing outputs on real traffic, a per-capability cutover rather than one switch, and a rehearsed rollback. And say aloud that this is more expensive than incremental replacement, so the decision is made knowingly.

What is an anti-corruption layer?

A translation boundary that stops another system's model leaking into yours. When you integrate with a legacy system or a vendor, its concepts arrive with its naming, its nullable fields and its odd state machine, and if those types propagate into your domain you have inherited its design permanently. The anti-corruption layer maps them at the edge into your own vocabulary and rejects what does not translate. It is the pattern that makes strangler-fig migration survivable, because the new service can hold a clean model while still reading legacy data. Its cost is a mapping layer that must be maintained and that hides genuine mismatches unless the failures are surfaced loudly.

Show me how the data moves during an incremental migration.

Behaviour is the easy half. The data is where migrations actually stall.

phase                new writes     new reads      authority   revert by
-------------------  -------------  -------------  ----------  --------------
0  facade only       legacy         legacy         legacy      route change
1  shadow            legacy         legacy + new,  legacy      route change
                                    compared
2  dual write        legacy + new   legacy         legacy      route change
3  read switch       legacy + new   new            legacy      route change
4  authority move    new            new            new         restore + replay
5  contract          new            new            new         none

Backfill runs between 2 and 3, in batches, with a reconciliation job that
counts and checksums both stores and alerts on drift above zero rows.

The column that matters is the last one. Phases 0 to 3 revert by flipping a route because the legacy store is still authoritative and still current, which means four of the six steps are cheap to undo — and that is what makes the migration tolerable to a business that has been burned before.

Phase 4 is the one-way door, and it should be scheduled and communicated as such. Everything before it is rehearsal; after it, reverting means restoring a backup and replaying whatever the new system accepted since. Naming that step explicitly in the plan is a senior signal, because the common failure is drifting across it by accident and discovering the fact during an incident.

Dual write in phase 2 is not the same as the dual-write anti-pattern, provided only one store is authoritative and a reconciliation job is running. The reconciliation is not optional: without it, dual write means two stores that quietly disagree, and you will find out at the read switch when the numbers change.

Phase 5 is the one that gets deferred forever. Removing the dual write, the comparison job and the legacy path is where the ongoing cost of the migration finally goes away, and if it is not funded the programme has permanently increased the system count.

How do you know a migration is finished?

When the old path is deleted, not when the new one works. The intermediate state carries the cost of both systems plus the synchronisation between them, so a migration that reaches ninety per cent and stops is the most expensive possible outcome — and it is the usual one, because the remaining ten per cent is the awkward capability with three users and no owner. The practical answer is to define completion up front as the removal of specific code, tables and infrastructure, put the decommissioning work in the same plan as the build, and report progress as legacy surface removed rather than new features delivered.

What is the second-system effect?

The tendency for a team's replacement of a system they know well to be over-general and over-featured, because every constraint they resented in the first one gets designed away at once. The result is late, complicated and often solves problems nobody has. It is worth knowing by name because it is the failure mode of exactly the situation where a rewrite feels most justified — the team understands the domain, has strong opinions about the old design, and reads restraint as timidity. The guard is a scope frozen against the current system's behaviour, with improvements queued explicitly as later work rather than folded into the replacement.

Decisions and documentation

What belongs in an ADR, and what does not?

Context with enough specificity that a reader can judge whether it still holds, the decision stated in one sentence in the active voice, the consequences including the ones you dislike, and the alternatives with the reason each was rejected. What does not belong: implementation detail that will change, a class-level design, anything about a decision cheap to reverse, and marketing. The test is whether it answers "why is it like this" for someone who joins in two years and is about to propose the thing you already rejected. An ADR that reads as justification rather than reasoning fails that test, because the reader cannot tell what was actually weighed.

When is a decision worth an ADR?

When it is expensive to reverse, when it will be questioned by people not in the room, or when it constrains other teams. That is a narrower set than it sounds: choosing the message broker, the tenancy model, the identity provider or the partition key of the largest table qualifies; choosing a logging library does not. Writing them for everything is how the practice dies, because a directory of two hundred documents is unsearchable and nobody trusts that the important ones are in there. Ten good ADRs for a system is a healthy number, and the discipline is in what you decline to write.

What are the C4 levels, and which one do people get wrong?

Context, container, component, code. Context shows the system as one box among users and external systems. Container shows the separately deployable things and the technology of each. Component shows the internals of one container. Code is class-level and is almost never worth drawing by hand because it dates instantly and a good IDE generates it. The level people get wrong is container: they either draw context and call it an architecture, which conveys nothing about how the system is built, or draw everything at once, producing the diagram with forty boxes that no one reads twice.

Show me a context diagram against a component diagram.

The same system at two altitudes, and the point is which conversation each one serves.

LEVEL 1 — CONTEXT  (audience: anyone, including non-technical)

  [Customer] ------> ( Ordering Platform ) ------> [Payment provider, ext.]
  [Warehouse staff]-^        |         |
                             v         v
                  [ERP, existing]   [Email provider, ext.]

  Six boxes. Says who uses it, what it depends on, and where money and
  data cross an organisational boundary. Says nothing about how it is built.


LEVEL 3 — COMPONENT  (audience: the team changing this container)

  Checkout API container
    +-- BasketController ----> PricingService ----> TaxRuleEngine
    |                              |
    |                              v
    +-- CheckoutController --> OrderFactory ----> OrderRepository --> [DB]
                                   |
                                   +-----------> OutboxWriter ----> [outbox]

  Says where a change to tax rules goes, and that the outbox write shares
  the transaction with the order write. Useless to anyone not editing this.

The altitudes are not better and worse, they are answers to different questions. Context answers "what is this system and what would break if it went away", which is the diagram for an executive, a security review, a new joiner in their first hour, or a vendor conversation. Component answers "where do I make this change", which is the diagram for the four people who own the container.

The reason to be explicit about the level is that mixed-altitude diagrams are the most common documentation failure. A picture showing the payment provider next to TaxRuleEngine has no audience: too much detail to explain the system, too little context to guide a change, and it will be wrong within a sprint because one of the two altitudes changes weekly and the other changes yearly.

That difference in decay rate is the practical argument. Context and container diagrams are worth maintaining because they change when the system genuinely changes. Component diagrams are worth drawing for a specific discussion and then allowing to die, and code-level diagrams are worth generating on demand and never committing.

In an interview, say which level you are drawing before you draw it. It reads as knowing that a diagram has an audience, which is most of what the documentation question is testing.

Why do architecture diagrams rot, and what survives?

Because they are maintained by goodwill and consulted rarely, so drift is invisible until someone acts on a diagram that has been wrong for a year. What survives is anything generated from the system — service dependency graphs from traces, infrastructure from the actual configuration — and anything so coarse-grained that it changes only when the architecture genuinely does, which is why context and container levels outlive component ones. The pragmatic position is to keep a small number of hand-drawn diagrams at high altitude, treat detailed ones as disposable artefacts of a specific discussion, and prefer diagrams-as-code in the repository so a reviewer notices when a change makes one false.

How do you document architecture for a mixed audience?

By writing separate artefacts rather than one that compromises. An executive wants one page: what the system does, what it costs, what the risks are, and what decision you need from them. An engineer joining the team wants the container diagram, the local setup, and the three surprising things about the codebase. An auditor wants data flows and boundaries. A single document serving all three serves none, and the usual result is a fifty-page specification nobody has finished. The reusable core is a short system overview plus a set of ADRs, with audience-specific pages assembled from that rather than duplicating it.

What does a design document do that an ADR does not?

A design document is written before the work and describes a whole change — motivation, options, proposed shape, migration, risks, testing, rollout — with the purpose of getting critique while changing course is still cheap. An ADR is narrow and durable: one decision, recorded so it can be understood later. Confusing them produces either ADRs that sprawl into specifications and stop being readable, or design documents with no extractable decisions, so a year on nobody can find why a choice was made without rereading forty pages. The practical pairing is one design document per significant project, from which two or three ADRs are extracted and kept.

Governance and standards

How do you run an architecture review that is not a gate?

Make it early, advisory and cheap. A review whose purpose is approval arrives too late to change anything, so teams route around it, and the reviewers see designs written to pass rather than designs. Instead offer it as a scheduled slot teams opt into while options are still open, with a short written document circulated beforehand, reviewers who ask questions rather than issue verdicts, and output that is a list of risks and recommendations owned by the team. Keep a small set of genuinely mandatory constraints — data residency, authentication, anything regulatory — and be explicit that those are the only gates.

Which architectural decisions should be delegated?

Everything that is reversible and local, which is most of them. If a team can change its mind next quarter without touching anyone else's code or migrating data, the decision is theirs and centralising it buys latency and resentment rather than quality. Keep central the choices that bind the organisation: cross-cutting concerns like identity and observability, anything with a compliance or data-residency dimension, the interfaces between teams, and commitments to a vendor. Delegation is not abdication, though — it requires a written boundary of what is delegated and a way for the centre to see the aggregate consequences of many local choices.

How do you set a standard that people actually follow?

By making the standard the easiest path rather than the required one. A document saying all services must emit structured logs with a trace identifier gets partial compliance; a service template that already does it, plus a library that makes it the default, gets near-total compliance and no enforcement effort. Where a standard cannot be embodied in a template, make conformance automatically visible — a dashboard of which services meet it — before making it mandatory, so the conversation is about specific gaps. Standards imposed without a paved road generate either quiet non-compliance or genuine harm to teams whose case the standard did not anticipate.

Show me build versus buy as a five-year cost comparison.

The mistake is comparing licence cost to development cost, so the model has to include the years after the first.

Requirement: feature-flagging and staged rollout across 30 services

BUY  vendor SaaS
  licence, 30 services, growing seats     yr1  38,000   yr2-5  46,000 each
                                          38,000 + (4 x 46,000)  = 222,000
  integration, 1 engineer for 6 weeks     yr1  18,000   =          18,000
  ongoing ownership, 0.1 FTE              14,000 x 5 yrs =          70,000
                                                                 ---------
  5-year total                                                     310,000

BUILD  in-house service
  initial build, 2 engineers x 4 months   yr1 200,000   =         200,000
  infrastructure and on-call              16,000 x 5 yrs =         80,000
  maintenance and features, 0.4 FTE       56,000 x 4 yrs (2-5) =  224,000
                                                                 ---------
  5-year total                                                     504,000

Difference over five years: build costs 1.63x buy, 194,000 more.
Break-even on licence alone: build 504,000 less the buy-side costs that
are not licence (18,000 integration + 70,000 ownership = 88,000) leaves
416,000 of licence budget, so ~83,000/yr against today's ~44,400/yr
average. The vendor would have to be about 1.9x more expensive than
quoted before building wins on cost.

The line that decides it is maintenance, and it is the line always omitted. The build column's four-tenths of an engineer forever is not padding: someone answers the pages, patches the dependencies, adds the feature the third team needs, and onboards their replacement. Over years two to five that line alone reaches 224,000, more than the 200,000 initial build it is supposed to be the cheap tail of, which is why "we could write that in a month" is usually true and usually irrelevant.

Buy wins here, and by a wide enough margin — 310,000 against 504,000 — that the conclusion does not turn on any single assumption. It survives the licence growing faster than modelled, because the vendor would have to charge roughly twice the quoted rate before the columns cross, and it survives the build coming in under estimate, because the initial build is less than half of the build column. What it would not survive is the maintenance line being deleted, which is precisely the edit that gets made when someone wants the answer to be build.

What the arithmetic does not settle is strategy, and this is where a senior answer diverges. Buy anything that is not your differentiator, even at a premium, because the scarce resource is engineering attention rather than money. Build what you sell. Feature flagging is nobody's product, so the numbers and the strategy agree here; where they disagree — a vendor is cheaper but owns the data model of your core domain — the strategy wins.

Three costs to name that resist quantification. Exit cost, because migrating off a vendor whose data model you have adopted is a project, so ask what leaving looks like before signing. Roadmap risk, since the feature you need may never arrive and you cannot build it. And integration surface: thirty services calling a vendor SDK is a coupling you now version.

The buy-side trap is the seat-count curve. A licence priced per service or per seat grows with your success, so model it against the headcount plan rather than today's, and the break-even year moves.

How do you manage technical debt as a portfolio rather than a backlog?

By tracking the interest rather than the principal. A backlog of debt tickets is an unranked list that loses to features every quarter, because nothing on it states what it costs to keep. A portfolio view records, per item, what the debt makes slower or riskier, in units someone cares about — hours per sprint lost, an incident class it causes, a change it blocks — and what remediation costs. Then it ranks by interest and pays down deliberately, accepting some debt permanently because the interest is genuinely low. That reframing is what lets debt work compete for capacity honestly instead of being smuggled in.

Show me a technical-debt register with interest.

The difference from a to-do list is that every row states what carrying it costs.

item                     interest paid now              principal   verdict
-----------------------  -----------------------------  ----------  ----------
No test coverage on the  ~6 eng-hrs/sprint manual        3 weeks     pay now
pricing engine           regression; 2 escaped bugs
                         per quarter

Shared orders schema     every release needs 3-team      8 weeks     pay next,
across 4 services        coordination; ~1 week           (staged)    it blocks
                         slippage per release                        the roadmap

Node 16 in the reporting no functional impact;            1 week      pay now,
service                  fails security scan, blocks                 cheap and
                         the compliance audit in Q4                  dated

Hand-rolled retry logic  1-2 incidents/yr, 4 hrs each    2 weeks     accept,
in 3 clients                                                         low interest

Bespoke admin UI on a    1 eng-week/yr; only 4 internal  6 weeks     accept
dead framework           users; no security exposure                 indefinitely

Two of the five rows are accepted, and that is the register working. Debt with low interest is cheap financing and paying it down is a poor use of capacity; declaring so in writing is what stops the item being re-raised every planning session and what makes the rest of the list credible.

The interest column has to be in units a product manager can trade against a feature. "Six engineering hours per sprint plus two escaped bugs a quarter" is comparable to a roadmap item; "the code is messy" is not, and loses every time it is proposed. Where the interest cannot be quantified at all, that is usually evidence it is aesthetic rather than economic.

The third row shows why interest is not the only axis. The Node version costs nothing today and becomes an audit failure on a known date, so it is a step function rather than a rate — debt with a deadline outranks debt with a higher current cost but no cliff. Any register worth keeping distinguishes the two.

The second row is the one to lead with in a stakeholder conversation, because its interest is measured in release slippage and it blocks named roadmap items. That is the argument that gets remediation funded: not that the architecture is imperfect, but that this specific item is charging a week per release and the thing they asked for is behind it.

How do you say no to a requirement?

By pricing it rather than refusing it. "No" invites escalation; "yes, and here is what it costs and what it displaces" moves the decision to the person who owns the trade-off, which is where it belongs. So quantify the cost, name what slips, offer the cheaper approximation if one exists — an audit log instead of full event sourcing, a nightly export instead of real-time sync — and ask which problem the requirement was solving, because requirements arrive as solutions surprisingly often. Keep the outright refusal for things that are unsafe, illegal or irreversible, and when you use it, say which of the three.

Interview traps

How do you disagree with a senior stakeholder on a technical point?

In private first, on the consequence rather than the choice. "I think this commits us to a data migration we cannot reverse, and here is what that costs in the third quarter" is a different conversation from "that is the wrong technology", because it gives them new information rather than a competing opinion. Ask what they know that you do not, since senior people frequently have commercial context you lack. Then, if the decision goes against you, disagree and commit — visibly, because a leader who relitigates a lost decision in front of the team is more damaging than the decision. Record your reasoning in writing so the position is recoverable if the risk materialises.

What is an interviewer testing when they ask about a decision you regret?

Whether you can reason about your own work without either defending it or performing contrition. A strong answer names a real decision, states what you knew at the time and why it was defensible then, identifies the specific signal you missed, describes the cost as it actually landed, and says what you now do differently — a changed default, a question you ask earlier. A weak answer picks something trivially safe, blames the organisation, or offers a failure that is secretly a boast. The tell they are listening for is whether the lesson is specific enough to have changed behaviour, because a generic lesson means the reflection did not happen.

Why is "it depends" a bad answer, and when is it a good one?

It is bad when it ends there, because it declines the question and tells the interviewer nothing about your judgement. It is good when it names what it depends on, gives the answer for each branch, and then says which branch you think this case is in. "It depends on whether the write rate exceeds what one primary can take — below roughly five thousand writes a second I would not shard, above it I would, and the numbers we estimated put us under, so no" is a decision with its reasoning attached. The pattern generalises: name the variable, resolve it with an assumption, then commit.

What does a candidate reveal by the order in which they draw?

Almost everything. Starting with boxes and technology names before asking about users, volumes or the two hard requirements reads as pattern matching — the same diagram would have been drawn for a different problem. Starting with the requirements and the quality attributes, then the simplest structure that meets them, then adding machinery only where a named requirement forces it, gives every component a justification and makes the follow-up questions answerable. The other tell is what they do with a constraint that arrives mid-round: a strong candidate revisits an earlier decision explicitly, and a weak one bolts a component onto the side of a design that no longer holds.

What single question most reliably separates candidates in an architecture round?

"What did that decision cost you?" It cannot be prepared for generically, because the answer has to be specific to a system the candidate actually operated. A strong answer names the cost in concrete terms — this is why our releases needed coordination for a year, this is the class of incident it created, this is what we would do differently and what we would keep — and distinguishes the costs that were foreseen and accepted from the ones that surprised them. A weak answer describes the benefits again, or reaches for a textbook trade-off that is not attached to anything that happened. The underlying test is whether the candidate has lived with a decision long enough to be taught by it, because architecture is mostly the judgement that comes from having been wrong at scale, and no amount of reading substitutes for it.