Application architecture & patterns
Middleware, MVC/layered + DI, the SOLID principles (one “smell it fixes” each), and the practical design patterns worth recognizing.
Architecture patterns exist to make change cheap and testing possible. The two things worth carrying into any interview: the SOLID principles as one-line “smells they fix,” and a handful of design patterns you can name the moment you see the problem they solve. Underneath both sits a single default structure — a request flowing through middleware into layers — that most real backends are some variation of.
The default shape: middleware into layers
Before the named patterns, internalize the structure most backends actually have. A request enters a middleware pipeline — a chain of functions, each handed the request and a next() to call. Each link does one cross-cutting job (parse the body, verify the token, log, rate-limit) and either passes control on or short-circuits (an auth middleware that returns 401 and never calls next()). Then the request reaches the layers: the controller translates HTTP to a method call, the service runs the business logic, the repository hides the database.
Why this shape wins: each layer has one job and depends only on the layer below through an interface, so you can unit-test the service with a fake repository, swap Postgres for an in-memory store, and never touch HTTP concerns in your business logic. That’s dependency injection doing the heavy lifting — and it’s the bridge from “structure” to the SOLID principle (DIP) that makes it possible.
SOLID — the smell each one fixes
| Principle | One-line meaning | Smell it fixes | Gotcha |
|---|---|---|---|
| SRP — Single Responsibility | a class has one reason to change | a ‘God class’ that does parsing + IO + business logic | over-splitting into anaemic one-method classes |
| OCP — Open/Closed | open to extension, closed to modification | editing a switch statement every time a new type appears | premature abstraction before the variation is real |
| LSP — Liskov Substitution | subtypes must be usable as their base type | a Square extends Rectangle that breaks callers | inheritance where composition was the right call |
| ISP — Interface Segregation | many small interfaces beat one fat one | implementing a huge interface with half the methods throwing | splitting so fine the interfaces lose meaning |
| DIP — Dependency Inversion | depend on abstractions, not concretions | a service hard-wired to a concrete MySQLClient | abstracting things that will never have a second impl |
The trap with SOLID is treating it as a checklist to maximize. Every principle has an over-application failure mode (the “Gotcha” column): SRP taken too far yields a swarm of one-method classes you can’t follow; OCP applied before any real variation exists is just premature abstraction. The skill is recognizing the smell first — “I keep editing this switch whenever a new payment type appears” — and then reaching for the principle as the cure, not sprinkling abstractions prophylactically.
Design patterns worth recognizing
You rarely implement these from a textbook — you recognize the situation and reach for the shape.
| Pattern | Use when | Avoid when | Gotcha |
|---|---|---|---|
| Factory | object creation is conditional/centralized | construction is trivial | hides which concrete type you got |
| Singleton | exactly one shared instance (config, pool) | you value testability — it's global state | hard to mock; hidden coupling across the app |
| Strategy | swap an algorithm at runtime (sort, pricing) | there's only ever one behavior | can proliferate tiny classes |
| Repository | abstract data access behind an interface | trivial single-table CRUD | leaky abstraction over a rich ORM |
| Adapter | bridge two incompatible interfaces | you control both sides — just change one | adapter chains hide cost |
| Observer (pub/sub) | many parts react to an event | a direct call is clearer | untraceable event spaghetti |
| Decorator | layer behavior (logging, caching) without editing the class | the base class is fine to change | deep wrapping is hard to debug |
A useful way to group them: creational (Factory, Singleton) decide how objects are made; structural (Adapter, Decorator, Repository) decide how objects are composed and wrapped; behavioral (Strategy, Observer) decide how objects collaborate at runtime. The two you’ll name most in interviews are Strategy (swap a charging or pricing algorithm without touching the caller) and Decorator (wrap a handler with logging or caching without editing it — which is also conceptually what middleware is).
// SMELL: service constructs its dependency and hard-codes the algorithm
class Checkout {
pay(amount: number) {
const gateway = new StripeClient(); // hard-wired concretion (DIP violation)
return gateway.charge(amount);
}
}
// FIX: inject an abstraction; swap implementations as a Strategy
interface PaymentGateway { charge(amount: number): Promise<Receipt>; }
class Checkout {
constructor(private gateway: PaymentGateway) {} // DIP: depend on the interface
pay(amount: number) { return this.gateway.charge(amount); }
}
// Tests pass a FakeGateway; prod passes StripeGateway or PaypalGateway.One small change buys two patterns at once. The injected interface satisfies DIP (depend on abstraction, not new StripeClient()) and lets you swap gateways at runtime as a Strategy. The payoff is testability: Checkout no longer reaches out to the network, so a unit test passes a FakeGateway that returns a canned Receipt and asserts on the logic — fast, deterministic, no Stripe account required.
01 Learning objectives
0 / 4 done02 Curated reading
03 Knowledge check
- 01easy
Dependency injection mainly improves:
- 02medium
You added a new payment type and had to edit a giant switch statement. Which SOLID principle did the design violate?
- 03medium
Which pattern swaps an algorithm at runtime behind a common interface?
04 Interview questions
browse all ↗What gets asked on this topic — tap a card for how to approach it, the follow-ups, and the trap. Company tags are best-effort & sourced.
-
Give a one-line 'smell it fixes' for each SOLID principle.
S — Single Responsibility: a class has one reason to change; fixes the god-class that mixes parsing, business rules, and DB code. O — Open/Closed: extend behavior without editing existing code; fixes the ever-growing switch you reopen for every new case. L — Liskov Substitution: subtypes must be usable through the base type without surprises; fixes the subclass that throws on a method the parent promises. I — Interface Segregation: many small interfaces over one fat one; fixes clients forced to implement methods they don't use. D — Dependency Inversion: depend on abstractions, not concretions; fixes high-level logic nailed to a specific DB/SDK, which kills testability.
Follow-ups they push on- Which SOLID principle most directly enables unit testing? (DIP)
- Give a concrete Liskov violation.
Red flag Reciting the names without a concrete smell. Interviewers want the problem each one removes, not the dictionary definition.
source: GeeksforGeeks — SOLID principles ↗ -
Why is the Singleton pattern considered a testability and design smell?
A Singleton enforces one global instance with global access. The problems: it's global mutable state in disguise, which hides dependencies (a class secretly reaches for
Logger.getInstance()instead of receiving it). That makes unit tests hard — you can't easily substitute a mock, tests share state and leak into each other, and parallel tests interfere.The usual fix is dependency injection: create one instance at the composition root and pass it in. You keep 'one instance' as a lifecycle policy without the hard-coded global lookup.
Follow-ups they push on- How does DI give you 'one instance' without the Singleton anti-pattern?
- When is a Singleton actually fine?
Red flag Defending Singleton as 'just one object'. The cost is the static global access point that hides dependencies and breaks test isolation.
source: GeeksforGeeks — Singleton design pattern ↗ -
Explain dependency injection and how it improves testability.
Dependency injection means a component receives its collaborators from outside (constructor/parameters) instead of constructing them itself. It's the practical expression of the Dependency Inversion Principle: code depends on an interface, and the concrete implementation is wired in at the edge.
Testability win: in a test you inject a fake/mock repository or HTTP client, so you can unit-test the service in isolation with no real database or network. It also decouples modules — swapping Postgres for an in-memory store is a wiring change, not a rewrite.
Follow-ups they push on- How does this relate to the Repository pattern?
- Constructor injection vs a service locator — which is cleaner and why?
Red flag Confusing DI with 'using a DI framework'. DI is just passing dependencies in; the container is optional sugar.
source: Martin Fowler — Inversion of Control & DI ↗ -
Walk through layered architecture (controller → service → repository). What belongs in each layer?
Controller: HTTP concerns only — parse/validate the request, call a service, map the result to a status code and response. Service: the business logic and orchestration — transactions, rules, coordinating multiple repositories; it knows nothing about HTTP. Repository: data access — encapsulates queries behind a collection-like interface so the service depends on an abstraction, not raw SQL.
The payoff is that each layer is testable and replaceable in isolation, and business logic doesn't leak into the web framework or the database.
Follow-ups they push on- Why keep HTTP concerns out of the service layer?
- Where does request validation live, and where do domain rules live?
Red flag Fat controllers with business logic and SQL inline — you lose testability and the logic gets tied to the web framework.
source: Martin Fowler — Patterns of Enterprise Application Architecture ↗ -
What is middleware in a web framework, and what does it look like in practice?
Middleware is a function in the request/response pipeline that runs before (and often after) the route handler. Each piece can inspect or mutate the request/response and either pass control to the next link or short-circuit (e.g. reject an unauthenticated request).
Classic uses: logging, authentication, body parsing, CORS, rate limiting, error handling. In Express the signature is
(req, res, next) => { ... next(); }. The ordered chain is what makes cross-cutting concerns composable instead of duplicated in every handler.Follow-ups they push on- How does calling (or not calling) next() control the chain?
- Why is error-handling middleware registered last?
Red flag Forgetting to call next() (or to send a response), which hangs the request silently.
source: Express — Using middleware ↗ -
Strategy vs Factory vs Adapter — give a one-sentence use case for each.
Strategy: swap interchangeable algorithms behind one interface at runtime — e.g. pluggable payment processors or sort comparators, picked by configuration. Factory: centralize object creation so callers ask for *what* they want, not *how* it's built — e.g.
createParser(fileType). Adapter: wrap an incompatible third-party interface to match the one your code expects — e.g. adapting a legacy SDK to yourPaymentGatewayinterface.Mnemonic: Strategy varies behavior, Factory varies construction, Adapter reconciles interfaces.
Follow-ups they push on- Strategy vs simple if/else — when is the pattern worth it?
- How does Adapter differ from Decorator?
Red flag Applying a pattern for its own sake. A two-branch conditional doesn't need Strategy; patterns earn their cost when the variation is open-ended.
source: Refactoring Guru — Design patterns catalog ↗ -
Explain the Observer (pub/sub) pattern and the Decorator pattern. Give a real backend use of each.
Observer / pub-sub: subjects publish events and any number of subscribers react, with no direct coupling between them — e.g. on
OrderPlaced, the email service, inventory service, and analytics each subscribe independently. It decouples producers from consumers and underlies event-driven systems.Decorator: wrap an object to layer behavior without changing it, preserving the same interface — e.g. wrapping a repository with caching, then logging, then retry. Each layer adds one concern and delegates inward, so you compose features instead of editing the core class.
Follow-ups they push on- How does Observer relate to a message broker like Kafka?
- Decorator vs subclassing for adding logging — why prefer the decorator?
Red flag Confusing Decorator with Adapter. Decorator keeps the same interface and adds behavior; Adapter changes one interface into another.
source: Refactoring Guru — Observer ↗ -
Give a concrete Liskov Substitution Principle violation and how you'd fix it.
Classic example:
Square extends Rectangle. Setting width and height independently is part of Rectangle's contract, but a Square forces them equal, so code that doesrect.setWidth(5); rect.setHeight(4); assert area == 20breaks when handed a Square. The subtype violates the base type's expectations.Fix: drop the inheritance — model
Shapewith anarea()method and make Square and Rectangle siblings, or use immutable value objects so the mutating contract that conflicts never exists. The lesson: 'is-a' in English isn't enough; the subtype must honor the supertype's behavioral contract.Follow-ups they push on- Why is 'a square is a rectangle' true in math but wrong here?
- How does LSP relate to using exceptions in overridden methods?
Red flag Treating LSP as just 'subclasses should work'. The real test is behavioral substitutability — preconditions can't strengthen, postconditions can't weaken.
source: GeeksforGeeks — Liskov Substitution Principle ↗ -
What's the difference between MVC and a layered (controller/service/repository) architecture? Are they the same thing?
They overlap but aren't identical. MVC is a UI-organizing pattern: the Model holds data/state, the View renders it, and the Controller handles input and coordinates the two — its purpose is separating presentation from data.
A layered architecture stacks responsibilities by technical concern (presentation → business/service → data-access/repository), each layer depending only on the one below. In practice a server MVC framework's 'Controller' maps to the presentation layer, and the 'Model' often expands into service + repository layers. So MVC describes the request-handling triangle; layering describes the full vertical stack that the model side usually grows into.
What a strong answer coversMVC separates presentation (view) from data/state (model) via a controller.
Layered architecture separates by technical concern top-to-bottom.
MVC's 'Model' typically expands into service + repository layers.
They're complementary lenses, not competing choices.
Quick self-checkIn a layered backend, where does business logic (e.g. 'a refund can't exceed the original charge') belong?
-
No — the controller handles HTTP I/O and coordination, not domain rules.
-
Correct — business rules and orchestration live in the service layer.
-
No — the repository only abstracts data access.
-
No — that's presentation rendering.
Follow-ups they push on- Where does business logic live in a 'fat model' vs a service layer?
- Why is putting business logic in the controller a smell in both?
Red flag Cramming business logic and data access into the MVC controller. The controller is presentation/coordination; domain logic belongs in services, persistence in repositories.
source: MDN — MVC ↗ -
Explain hexagonal (ports & adapters) architecture. What problem does it solve over a plain layered design?
Hexagonal architecture puts the domain/application core at the center and defines ports (interfaces) for everything it talks to. Adapters implement those ports for specific technologies — a Postgres adapter, a REST adapter, a Kafka adapter — and plug in at the edges. The dependency rule points inward: the core never imports a framework or driver.
Versus a strict top-down layered design (where the business layer still depends on a concrete data layer beneath it), hexagonal inverts those edge dependencies so the database, web framework, and message bus are all swappable, interchangeable details. The payoff is testability (drive the core through fake adapters) and decoupling the domain from infrastructure churn.
What a strong answer coversDomain core + ports (interfaces) + adapters (tech-specific implementations).
Dependencies point inward; the core knows nothing about frameworks/drivers.
DB, web, and messaging become swappable adapters, not foundational layers.
Enables testing the core in isolation through fake adapters.
Follow-ups they push on- What's a 'driving' (primary) adapter vs a 'driven' (secondary) adapter?
- How does this relate to the Dependency Inversion Principle?
Red flag Letting domain code import the ORM/web framework directly 'for convenience'. That re-couples the core to infrastructure and defeats the whole ports-and-adapters point.
source: Alistair Cockburn — Hexagonal Architecture ↗ -
What does the Repository pattern give you, and what's the risk of a 'leaky' repository?
A Repository is a collection-like abstraction over persistence: the service asks for
userRepo.findActiveByEmail(email)and doesn't know whether that's SQL, a document store, or an in-memory list. It centralizes query logic, decouples the domain from the ORM, and makes services testable with a fake repository.The risk is a leaky abstraction: if the repository exposes
IQueryable, raw SQL fragments, or ORM-specific lazy-loading proxies, persistence concerns bleed into the service and the decoupling is gone. Keep the interface in domain terms — return domain objects, accept domain criteria — so the storage technology stays a private detail.What a strong answer coversCollection-like interface over persistence; hides the storage mechanism.
Decouples domain/service from the ORM and enables fake-based unit tests.
Centralizes query logic instead of scattering SQL across services.
Leak risk: exposing
IQueryable/raw SQL/lazy proxies re-couples callers to the DB.
Follow-ups they push on- Repository vs DAO — what's the conceptual difference?
- Why return domain objects rather than ORM entities directly?
Red flag Returning the ORM's query builder or lazy-loaded entities from the repository. Callers then depend on persistence details, defeating the abstraction.
source: Martin Fowler — Repository ↗ -
Trick: a class has 14 constructor parameters. Which design principle is being violated, and how do you fix it?
A bloated constructor (a 'too many dependencies' smell) usually signals a Single Responsibility Principle violation — the class is doing too many jobs, each pulling in its own collaborators. It's the constructor-injection symptom of a god class.
Fix by decomposing: extract cohesive groups of those dependencies into smaller focused classes (e.g. a
NotificationServicewrapping the email/SMS/push senders) so the original class depends on a few higher-level abstractions instead of fourteen low-level ones. The number of constructor args is a proxy metric; the real fix is restoring single responsibility, not hiding the args behind a service locator or a giant config object.What a strong answer coversMany constructor params → the class has too many responsibilities (SRP violation).
Constructor injection makes the bloat visible, which is a feature, not the bug.
Fix by extracting cohesive collaborators into focused sub-services.
Don't hide it with a service locator/God-config object — that masks the smell.
Quick self-checkA class needs 12 injected dependencies. The healthiest interpretation is:
-
Wrong — that hides the smell and harms testability.
-
Correct — too many dependencies signals too many responsibilities.
-
Wrong — wiring tooling doesn't reduce the responsibilities.
-
Wrong — reintroduces global state and breaks testing.
Follow-ups they push on- Why is hiding the dependencies behind a service locator the wrong fix?
- How does SRP relate to high cohesion?
Red flag 'Fixing' it by switching to a service locator so the dependencies become invisible. That hides the SRP violation instead of resolving it and hurts testability.
source: Refactoring Guru — Large Class smell ↗ -
Composition over inheritance — what does it mean and why is it usually the better default?
Inheritance models 'is-a' and binds a subclass to its parent's implementation at compile time — a rigid, white-box coupling that gets brittle with deep hierarchies (the fragile base class problem) and tempts Liskov violations. Composition builds behavior by holding other objects and delegating to them ('has-a'), which you can vary at runtime and swap for tests.
The guidance 'favor composition over inheritance' (from the Gang of Four) is about flexibility: small composed parts recombine freely, while inheritance hierarchies resist change. Use inheritance for genuine, stable is-a relationships with a real behavioral contract; prefer composition for sharing/reusing behavior.
What a strong answer coversInheritance = compile-time 'is-a', tight white-box coupling to the parent.
Composition = runtime 'has-a', delegate to swappable collaborators.
Deep hierarchies cause fragile-base-class and Liskov problems.
GoF guidance: favor composition; reserve inheritance for true, stable is-a.
Follow-ups they push on- How does the Strategy pattern embody composition over inheritance?
- When is inheritance still the right tool?
Red flag Reaching for inheritance to reuse a method, creating a deep hierarchy that's hard to change. If the relationship isn't a true is-a, compose and delegate instead.
source: Refactoring Guru — Favor composition over inheritance ↗ -
What is inversion of control (IoC), and how is dependency injection a specific form of it?
Inversion of control is the general principle that a framework or container — not your code — drives the flow: instead of your code calling into a library, the framework calls your code at the right moments ('don't call us, we'll call you', the Hollywood Principle). Event loops, middleware pipelines, and template method patterns are all IoC.
Dependency injection is one specific kind of IoC: inverting *who supplies a component's dependencies*. Rather than a class constructing its own collaborators, something external (a container or the composition root) provides them. So DI inverts dependency acquisition; IoC is the broader family of 'the framework controls the flow, you fill in the parts'.
What a strong answer coversIoC: the framework controls flow and calls your code ('Hollywood Principle').
DI is a specific form of IoC — inverting how dependencies are supplied.
Other IoC examples: event loops, callbacks, middleware, template method.
DI ≠ a DI container; the container is just one way to do DI.
Quick self-checkWhich statement is correct about IoC and DI?
-
Wrong — DI is a specific form of the broader IoC principle.
-
Correct — DI is a subset of inversion of control.
-
Wrong — an event loop is IoC with no DI container.
-
Wrong — it's a general principle (callbacks, template method, etc.).
Follow-ups they push on- Give a non-DI example of inversion of control.
- Why is 'IoC container' a slightly misleading name for a DI framework?
Red flag Using IoC and DI as synonyms. DI is one instance of IoC (inverting dependency supply); IoC is the broader idea of the framework owning control flow.
source: Martin Fowler — Inversion of Control ↗