الدرس 3 من 4
Architecture, Design, and Implementation
Where the line falls between the three, why arguing about the labels is a waste of time, and the one question that actually settles it.
Teams lose real hours to the question "is this an architecture decision or a design decision?" The argument is almost always a proxy for a different one: who gets to decide, and how much process the decision has to go through. Answering the real question directly is faster.
The three levels, by scope of consequence
- 1
Architecture — decisions that constrain many teams for a long time
Service boundaries, data ownership, synchronous versus asynchronous communication, the tenancy model, the authentication model. Wrong here means quarters of rework, and the people who pay are usually not the people who decided.
- 2
Design — decisions that shape one component
The internal layering of a service, which patterns its domain model uses, how its repository interfaces are shaped, how errors propagate within it. Wrong here means days or weeks of rework, contained inside one team.
- 3
Implementation — decisions inside one unit of work
Variable naming, whether a loop or a reduce, whether to extract a helper. Wrong here means a code review comment. These decisions matter enormously in aggregate and individually almost not at all.
The question that settles it
When a team is stuck on the label, replace the question with this one: who has to change their code if we change our minds? If the answer is "us, this sprint", it is design and the team should just decide. If the answer includes teams who are not in the room, it is architecture and it needs to be written down and communicated before it is built.
// ARCHITECTURE: payments talk to providers through a port the domain owns.
// Reversing this means every provider integration and every caller changes.
export interface PaymentGateway {
charge(intent: ChargeIntent): Promise<ChargeResult>;
}
// DESIGN: this slice models a failed charge as a value, not an exception.
// Reversing it means reworking this module's callers — one team, one sprint.
export type ChargeResult =
| { readonly ok: true; readonly reference: string }
| { readonly ok: false; readonly reason: DeclineReason; readonly retryable: boolean };
export class StripeGateway implements PaymentGateway {
async charge(intent: ChargeIntent): Promise<ChargeResult> {
// IMPLEMENTATION: the shape of this retry loop.
// Reversing it is a pull request.
for (let attempt = 0; attempt < 3; attempt += 1) {
const result = await this.attempt(intent);
if (result.ok || !result.retryable) return result;
}
return { ok: false, reason: 'provider_unavailable', retryable: false };
}
}Note what the architectural line buys in that example. Because the domain owns the `PaymentGateway` interface, adding a second provider is a new class and a configuration change — a design-level task for one team. Had the domain called the Stripe SDK directly, adding a provider would have been an architectural change touching every caller. Architecture is largely the practice of arranging things so that tomorrow's decisions are cheaper than today's.
Two ways to handle the boundary
Domain owns the interface
The business logic declares what it needs; infrastructure implements it. Dependencies point inward.
- A provider swap is additive, not invasive
- Domain logic is testable with no network and no SDK
- The interface documents exactly what the business actually needs
- One more layer to trace through when reading code
- Easy to over-apply and end up with an interface per class
Choose when: The dependency is genuinely replaceable, or you need the domain to be testable in isolation — which is nearly always true for anything touching money, identity or external providers.
Call the SDK directly
Business logic imports the vendor library and uses its types throughout.
- Immediately obvious what is happening
- No abstraction to maintain or explain
- Faster to write the first time
- Vendor types leak into business logic and into your tests
- Swapping the provider means editing every call site
- Domain tests need the SDK, and often the network
Choose when: The dependency is genuinely not going to change, is not on a critical path, and the code that uses it is small enough to rewrite in an afternoon — a logging client, not a payment provider.
Reacting to "we should abstract this"
As a developer
Adds an interface because abstraction is generally good practice, or resists it because YAGNI. Either way the argument is about principles, and it is settled by whoever argues longest or has the most seniority.
As an architect
Asks what the interface is protecting against and what it costs. If the answer is "we will plausibly need a second provider within a year and the domain must be testable without one", the interface is cheap insurance. If the answer is "it seemed cleaner", it is an unpaid tax on everyone who reads the code. The argument is settled by naming the risk, not by citing a principle.
Your team wants to change how errors are represented inside one service, from thrown exceptions to result objects. Architecture or design?RevealHide
Design — as long as the change stops at the service boundary. The HTTP status codes and error payloads the service returns are its contract, and those are architectural because other teams parse them. Internal error representation is the team's business. This is the general pattern: the contract is architecture, the implementation behind it is design, and keeping that line crisp is what lets a team refactor freely without a committee.
Key takeaways
- The three levels differ by scope of consequence, not by how sophisticated they are.
- Settle the label with "who has to change their code if we reverse this?" rather than by definition.
- The same decision can be architectural in one organisation and a detail in another.
- Good architecture makes tomorrow's decisions cheaper by keeping them inside one team.
- A component's external contract is architecture; everything behind it is the team's to decide.