All chapters in this track
- 01Chapter 1: Why Patterns Exist — Vocabulary, Not a Checklist
- 02Chapter 2: Patterns and SOLID: How They Relate
- 03Chapter 3: Creational Patterns I: Factory Method & Abstract Factory
- 04Chapter 4: Creational Patterns II: Builder, Singleton, Prototype
- 05Chapter 5: Structural Patterns I: Adapter & Facade
- 06Chapter 6: Structural Patterns II: Decorator & Proxy
- 07Chapter 7: Structural Patterns III: Composite, Bridge & Flyweight
- 08Chapter 8: Behavioral Patterns I: Strategy & Template Method
- 09Chapter 9: Behavioral Patterns II: Observer & Mediator
- 10Chapter 10: Behavioral Patterns III: Command & Memento
- 11Chapter 11: Behavioral Patterns IV: State & Chain of Responsibility
- 12Chapter 12: Behavioral Patterns V: Iterator & Visitor
- 13Chapter 13: Anti-Patterns and Overuse: Recognizing "Pattern Fever"
- 14Chapter 14: How Dynamic and Functional Languages Solve These Problems Differently
- 15Chapter 15: Architectural Patterns I: MVC/MVVM & Layered/Hexagonal Architecture
- 16Chapter 16: Architectural Patterns II: Event-Driven Architecture & CQRS
- 17Chapter 17: Architectural Patterns III: The Repository Pattern
- 18Chapter 18: Recognizing Which Pattern Fits an Interview Design Prompt
Chapter 15 of 18
Chapter 15: Architectural Patterns I: MVC/MVVM & Layered/Hexagonal Architecture
Why this matters
Every pattern so far in this book operates at the scale of a handful of collaborating classes. Architectural patterns operate at the scale of an entire application's structure — they answer "how do the major pieces of this whole system relate to each other?" rather than "how do these three classes collaborate?" Getting this scale of decision wrong is far more expensive to fix later than a misapplied Strategy, which is exactly why these patterns deserve their own, more careful treatment.
MVC (Model-View-Controller)
[BEGINNER] The problem it solves: presentation logic (how data is displayed), user input handling, and business/data logic tend to tangle together if not deliberately separated, making the UI hard to change without risking the business logic, and vice versa.
- Model — the data and business logic, with no knowledge of how it's displayed.
- View — the presentation layer, displaying the model's data, with minimal logic of its own.
- Controller — receives input, updates the model, and selects which view to render.
// Model — no knowledge of UI at all.
class TodoModel {
private items: string[] = [];
addItem(text: string): void { this.items.push(text); }
getItems(): string[] { return [...this.items]; }
}
// View — only knows how to render, not how state changes.
class TodoView {
render(items: string[]): void {
console.log("Todo list:", items.join(", "));
}
}
// Controller — connects input to model updates and triggers a re-render.
class TodoController {
constructor(private model: TodoModel, private view: TodoView) {}
addTodo(text: string): void {
this.model.addItem(text);
this.view.render(this.model.getItems());
}
}
const controller = new TodoController(new TodoModel(), new TodoView());
controller.addTodo("Write chapter 15"); // "Todo list: Write chapter 15"
When to use it: applications with a clear separation between data/logic and presentation, especially where the same model might need multiple views, or the view technology might change independently of business logic (a classic web-era pattern, still the backbone of most server-rendered web frameworks).
When to avoid it: very small applications where the ceremony of three explicit layers outweighs the benefit — a script or a tiny tool with one view and no real business logic doesn't need MVC's structure.
MVVM (Model-View-ViewModel)
[BEGINNER] MVVM is MVC's adaptation for data-binding-driven UI frameworks (common in modern reactive frontend frameworks): the ViewModel exposes state and commands the View binds to directly, and updates flow automatically through that binding rather than through an explicit Controller pushing updates to the View.
// ViewModel — exposes observable state; the View binds to it and updates itself automatically.
class TodoViewModel {
private items: string[] = [];
private listeners: (() => void)[] = [];
addItem(text: string): void {
this.items.push(text);
this.listeners.forEach(l => l()); // Observer pattern (Chapter 9) powering the data binding
}
getItems(): string[] { return [...this.items]; }
onChange(listener: () => void): void { this.listeners.push(listener); }
}
// The "View" subscribes once and re-renders itself whenever the ViewModel changes —
// no Controller explicitly pushing "now re-render" calls.
const viewModel = new TodoViewModel();
viewModel.onChange(() => console.log("Re-rendering:", viewModel.getItems().join(", ")));
viewModel.addItem("Learn MVVM");
[SENIOR INSIGHT] Notice that MVVM's ViewModel-to-View update mechanism is literally the Observer pattern from Chapter 9 — this is a recurring theme in architectural patterns: they're frequently composed out of the class-level patterns from Parts 2–3 of this book, applied at a larger scale, rather than being an entirely separate body of knowledge.
When to use it: UI frameworks with native data-binding support (most modern reactive frontend frameworks) — MVVM is usually the natural fit for the framework's own idioms, not a separate choice made independently of the framework.
When to avoid it: frameworks or environments without real data-binding support, where you'd be manually building the binding/notification machinery MVVM assumes exists — at that point, plain MVC (with the Controller explicitly triggering view updates) is simpler.
Layered Architecture
[BEGINNER] The problem it solves: without an enforced structure, code that talks to a database, code that implements business rules, and code that handles HTTP requests all tend to tangle together (this is the exact concern raised in the OOP book's Chapter 4 abstraction-layering table). Layered architecture makes the separation explicit and enforced: each layer only calls the layer directly below it.
+------------------------+
| Presentation Layer | (HTTP controllers, CLI handlers)
+------------------------+
| Business Layer | (domain logic, validation, orchestration)
+------------------------+
| Data Access Layer | (repositories, ORM/queries)
+------------------------+
| Database |
+------------------------+
// Data Access Layer — knows about persistence, nothing else.
class UserRepository {
findById(id: string): { id: string; email: string } { return { id, email: "user@example.com" }; }
}
// Business Layer — knows domain rules, depends only on the layer below.
class UserService {
constructor(private repository: UserRepository) {}
getUserEmail(id: string): string {
const user = this.repository.findById(id);
return user.email;
}
}
// Presentation Layer — knows about HTTP, depends only on the layer below.
class UserController {
constructor(private service: UserService) {}
handleGetUserEmail(id: string): { status: number; body: string } {
return { status: 200, body: this.service.getUserEmail(id) };
}
}
When to use it: most business applications with real, non-trivial domain logic, a persistence layer, and an entry point (HTTP/CLI/etc.) — this is the default, well-understood shape for the vast majority of business software.
When to avoid it: it can add unnecessary indirection for very simple CRUD applications with essentially no business logic between the HTTP layer and the database — in that specific case, the "business layer" is often just a thin, redundant pass-through.
Hexagonal Architecture (Ports and Adapters)
[SENIOR INSIGHT] Layered architecture's weakness: business logic in the middle layer still typically depends downward on the data access layer's interfaces, which means the business logic's tests often need at least a fake/mock of that lower layer, and the direction of dependency still points toward infrastructure. Hexagonal architecture (Ports and Adapters) inverts this: business logic sits at the center and defines the interfaces ("ports") it needs; infrastructure ("adapters" — the database, the HTTP framework, external APIs) implements those interfaces and depends inward on the business logic's terms, not the other way around.
// The "port" — defined by the business logic, in terms the business logic cares about.
interface UserRepositoryPort {
findById(id: string): { id: string; email: string };
}
// Business logic (the "hexagon's core") depends only on the port, never on a concrete database.
class UserService {
constructor(private repository: UserRepositoryPort) {}
getUserEmail(id: string): string { return this.repository.findById(id).email; }
}
// An "adapter" — infrastructure implementing the port, on the business logic's terms.
class PostgresUserRepositoryAdapter implements UserRepositoryPort {
findById(id: string): { id: string; email: string } {
return { id, email: "from-postgres@example.com" }; // real SQL would live here
}
}
// A second adapter — swapped in for tests, with zero changes to UserService:
class InMemoryUserRepositoryAdapter implements UserRepositoryPort {
findById(id: string): { id: string; email: string } { return { id, email: "test@example.com" }; }
}
This is Dependency Inversion (OOP book, Chapter 10) applied at the scale of an entire application's architecture rather than a single class relationship — another example of an architectural pattern being class-level principles, scaled up.
When to use it: business logic that needs to be tested independently of infrastructure, or systems that genuinely need to swap infrastructure (different databases, different message brokers) without touching business rules — common in domain-heavy applications where the business logic is complex enough to deserve strong isolation.
When to avoid it: simple applications where layered architecture's more familiar, lower-ceremony structure is sufficient — Hexagonal's inversion is a real, deliberate investment, best justified by a genuine testing or infrastructure-swapping need.
Common mistakes
- Choosing MVVM because it's fashionable, in a framework/environment without real data-binding support — this means manually building the notification machinery MVVM assumes the framework provides, which is more work than plain MVC would have been.
- Adding a full layered architecture (with a distinct "business layer") to a trivial CRUD app with no real business logic — the business layer becomes a pointless pass-through, adding files without adding value.
- Calling something "Hexagonal" while still letting business logic import a concrete infrastructure class directly somewhere — this silently breaks the entire inversion the pattern exists to provide; the discipline (business logic depends only on ports, never on concrete adapters) has to be enforced everywhere, not just in the primary example.
- Not recognizing that these architectural patterns are compositions of class-level patterns (Observer inside MVVM, Dependency Inversion inside Hexagonal) — missing this connection makes each architectural pattern feel like new, unrelated knowledge instead of familiar principles applied at scale.
Discussion / interview questions
-
"What's the difference between MVC and MVVM, and when would you choose one over the other?" Strong answer: MVC has an explicit Controller pushing updates to the View; MVVM relies on data-binding between View and ViewModel, updating automatically. Choice usually follows the framework: use MVVM in frameworks with native data-binding/reactive support, MVC otherwise.
-
"How does Hexagonal Architecture differ from a standard layered architecture, given both separate business logic from infrastructure?" Strong answer: Layered architecture still has business logic depending downward on data-access-layer interfaces; Hexagonal inverts this so business logic defines its own ports and infrastructure adapts to them, depending inward — the same shape as Dependency Inversion, applied architecturally.
-
"Why might you avoid Hexagonal Architecture for a simple CRUD application?" Strong answer: The isolation and swappability it provides are a deliberate investment best justified by real, complex business logic needing independent testing or genuine infrastructure-swapping needs — for a simple CRUD app, layered architecture's lower ceremony is usually sufficient.
-
"How does MVVM's update mechanism relate to a pattern covered earlier in this book?" Strong answer: It's the Observer pattern (Chapter 9) — the ViewModel is the subject, the View is (effectively) the observer, and updates propagate through subscription/notification rather than an explicit push from a controller.
Chapter summary / cheat sheet
- MVC: Model (data/logic) + View (presentation) + Controller (input handling, explicit update-pushing) — the long-standing default, especially for server-rendered applications.
- MVVM: Model + View + ViewModel, updates flow via data-binding (Observer under the hood) rather than explicit Controller pushes — fits frameworks with native reactive/binding support.
- Layered Architecture: presentation → business → data access → database, each layer depending only on the one below — the default shape for most business applications with real domain logic.
- Hexagonal (Ports and Adapters): business logic at the center defines ports; infrastructure adapts inward to them — Dependency Inversion applied architecturally, justified by real testing/infrastructure-swapping needs.
- All four are, at core, class-level patterns and principles from earlier in this book (Observer, Dependency Inversion) applied at the scale of a whole application's structure.