> cs·fundamentals
interview 0% 30m read
2.4 [J][A] 14 interview Q's

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.

A request passes through three middleware boxes (logging, auth, body parsing), then into a controller, then a service, then a repository, then the database; arrows point right through the chain.request →loggingmiddlewareauthmay 401 herebody parsemiddlewarecontrollerspeaks HTTPservicebusiness logicrepositorydata accessDatabase
FIG 1 · middleware + layered flow Cross-cutting concerns live in middleware; business logic flows controller → service → repository. Each layer knows only the one below it.

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

PrincipleOne-line meaningSmell it fixesGotcha
SRP — Single Responsibilitya class has one reason to changea ‘God class’ that does parsing + IO + business logicover-splitting into anaemic one-method classes
OCP — Open/Closedopen to extension, closed to modificationediting a switch statement every time a new type appearspremature abstraction before the variation is real
LSP — Liskov Substitutionsubtypes must be usable as their base typea Square extends Rectangle that breaks callersinheritance where composition was the right call
ISP — Interface Segregationmany small interfaces beat one fat oneimplementing a huge interface with half the methods throwingsplitting so fine the interfaces lose meaning
DIP — Dependency Inversiondepend on abstractions, not concretionsa service hard-wired to a concrete MySQLClientabstracting things that will never have a second impl
Each SOLID letter is a named fix for a specific maintainability smell.

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.

PatternUse whenAvoid whenGotcha
Factoryobject creation is conditional/centralizedconstruction is trivialhides which concrete type you got
Singletonexactly one shared instance (config, pool)you value testability — it's global statehard to mock; hidden coupling across the app
Strategyswap an algorithm at runtime (sort, pricing)there's only ever one behaviorcan proliferate tiny classes
Repositoryabstract data access behind an interfacetrivial single-table CRUDleaky abstraction over a rich ORM
Adapterbridge two incompatible interfacesyou control both sides — just change oneadapter chains hide cost
Observer (pub/sub)many parts react to an eventa direct call is cleareruntraceable event spaghetti
Decoratorlayer behavior (logging, caching) without editing the classthe base class is fine to changedeep wrapping is hard to debug
Name the problem first, then the pattern — not the other way round.

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).

DIP + Strategy in one small refactor
// 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 done

02 Curated reading

03 Knowledge check

knowledge check3 questions · pass ≥ 70%
  1. 01easy

    Dependency injection mainly improves:

  2. 02medium

    You added a new payment type and had to edit a giant switch statement. Which SOLID principle did the design violate?

  3. 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.

  • Commonly asked mid concept common 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.

    Red flag Reciting the names without a concrete smell. Interviewers want the problem each one removes, not the dictionary definition.

    source: GeeksforGeeks — SOLID principles ↗
  • Commonly asked mid trick common 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.

    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 ↗
  • Commonly asked mid concept common 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.

    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 ↗
  • Commonly asked mid concept common 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.

    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 ↗
  • Commonly asked junior concept common 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.

    Red flag Forgetting to call next() (or to send a response), which hangs the request silently.

    source: Express — Using middleware ↗
  • Commonly asked mid concept occasional 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 your PaymentGateway interface.

    Mnemonic: Strategy varies behavior, Factory varies construction, Adapter reconciles interfaces.

    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 ↗
  • Commonly asked mid concept occasional 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.

    Red flag Confusing Decorator with Adapter. Decorator keeps the same interface and adds behavior; Adapter changes one interface into another.

    source: Refactoring Guru — Observer ↗
  • Commonly asked senior debug occasional 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 does rect.setWidth(5); rect.setHeight(4); assert area == 20 breaks when handed a Square. The subtype violates the base type's expectations.

    Fix: drop the inheritance — model Shape with an area() 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.

    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 ↗
  • ★ must-know Commonly asked mid concept common 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 covers
    • MVC 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-check

    In a layered backend, where does business logic (e.g. 'a refund can't exceed the original charge') belong?

    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 ↗
  • Commonly asked senior concept occasional 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 covers
    • Domain 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.

    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 ↗
  • Commonly asked mid concept common 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 covers
    • Collection-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.

    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 ↗
  • Commonly asked mid trick occasional 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 NotificationService wrapping 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 covers
    • Many 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-check

    A class needs 12 injected dependencies. The healthiest interpretation is:

    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 ↗
  • Commonly asked mid concept common 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 covers
    • Inheritance = 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.

    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 ↗
  • Commonly asked senior concept occasional 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 covers
    • IoC: 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-check

    Which statement is correct about IoC and DI?

    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 ↗