Lesson 22 / 25

Architectural Patterns Overview

MVC, repository, unit of work and a pointer to CQRS.

Patterns at a larger scale

Some patterns organise whole applications rather than a few classes. MVC (Model-View-Controller) separates data and rules (model), presentation (view) and input handling (controller); variants include MVP and MVVM. The Repository pattern (from Martin Fowler's Patterns of Enterprise Application Architecture and Domain-Driven Design) gives a collection-like interface for loading and saving domain objects, hiding SQL or ORM details from business logic. Unit of Work tracks changes made during a business operation and commits them together, usually in one transaction; many ORMs implement it for you (for example a session or context object). CQRS (Command Query Responsibility Segregation) uses separate models for writes and reads; it can help with complex domains or very different read and write loads, but adds significant complexity and is not a default choice.

From catalogue to judgement

Patterns at the architecture level, the smells that signal misuse, and how to talk about patterns.

Four ideas: architectural patterns, anti-patterns, interviews and reviews, a final checklist.
Figure 8.1 — Architecture, smells, communication and review.

Repository and unit of work

Business logic talks to interfaces; one transaction commits the changes.

interface AccountRepository {
  findById(id: string): Promise<Account | null>;
  save(account: Account): Promise<void>;
}

interface UnitOfWork {
  accounts: AccountRepository;
  ledger: LedgerRepository;
  commit(): Promise<void>;
  rollback(): Promise<void>;
}

async function transferFunds(uowFactory: () => Promise<UnitOfWork>, fromId: string, toId: string, cents: number) {
  const uow = await uowFactory();          // e.g. begins a database transaction
  try {
    const from = await uow.accounts.findById(fromId);
    const to = await uow.accounts.findById(toId);
    if (!from || !to) throw new Error('Account not found');
    from.withdraw(cents);                   // domain rules live on the model
    to.deposit(cents);
    await uow.accounts.save(from);
    await uow.accounts.save(to);
    await uow.ledger.record({ fromId, toId, cents });
    await uow.commit();                     // all or nothing
  } catch (err) {
    await uow.rollback();
    throw err;
  }
}

Do not wrap an ORM twice

If your ORM already provides a repository-like API and a unit of work, a generic repository layer on top often adds code without adding value. Introduce one when you need to hide persistence from domain logic or test without a database.

Quick check: What does the Unit of Work pattern do?

  • Separates read and write databases
  • Renders HTML views
  • Ensures only one database connection exists
  • Tracks changes in a business operation and commits them together
Answer

Tracks changes in a business operation and commits them together — It groups changes into one atomic commit.