Enterprise Application Patterns

Learning Objectives

Compare Repository, Unit of Work, Data Mapper, Active Record, Service Layer, DTO, Identity Map, Lazy Load and Specification.

Get an overview of CQRS.

Treat these as options with trade-offs.

Prerequisites: Modules 51, 62, 71, 74.

Intuition and Definition

Active Record The object knows how to save itself (order.save())

Simple CRUD; couples domain to the database Data Mapper A separate mapper moves data between objects and tables

Domain independent of persistence; more code (ORMs automate it)

Collection-like interface for aggregates Clean domain API; can hide important query costs Unit of Work

Tracks new/changed/removed objects and commits them together

One transaction, ordered writes; memory for tracking Identity Map One object per row per session

Consistency, caching; stale data in long sessions Load related data on first access Less upfront loading; N+1 risk Service Layer Application operations over the domain

Clear boundary; may become anemic transaction scripts Data shaped for transfer Decoupling; mapping code Query/business criteria as objects

Reusable rules; can leak into persistence Separate models for commands (writes) and queries (reads)

Optimized reads/writes; more moving parts, often eventual consistency

Why It Matters

Every business application must move data between objects and storage. Knowing the vocabulary and trade-offs lets you choose deliberately — and recognise what your ORM already does.

Syntax and Language Notes

Entity Framework's DbContext is a Unit of Work + Identity Map; DbSet<T> is repository-like; Rails' ActiveRecord and Django models are Active Record; Hibernate/SQLAlchemy are Data Mappers.

Complete Example

A Unit of Work with an Identity Map over a tiny in-memory "database", showing change tracking and a single commit:

class Database:                                   # pretend storage with a statement log
    def __init__(self): self.rows, self.log = {}, []
    def select(self, id): self.log.append(f"SELECT {id}"); return dict(self.rows[id]) if id in self.rows else None

class Vehicle:
    def __init__(self, vin, km): self.vin, self.km = vin, km

class UnitOfWork:
    def __init__(self, db):
        self.db, self.identity_map, self.snapshots, self.new = db, {}, {}, []
    def get(self, vin):                            # identity map: one object per row per unit of work
        if vin not in self.identity_map:
            row = self.db.select(vin)
            if row is None: return None
            v = Vehicle(vin, row["km"])
            self.identity_map[vin], self.snapshots[vin] = v, dict(row)
        return self.identity_map[vin]
    def add(self, v): self.new.append(v)
    def commit(self):
        stmts = [f"INSERT {v.vin} km={v.km}" for v in self.new]
        for vin, v in self.identity_map.items():   # change tracking: compare with snapshot
            if self.snapshots[vin]["km"] != v.km:
                stmts.append(f"UPDATE {vin} km={v.km}")
        self.db.log.append("BEGIN"); self.db.
…
same object: True
SELECT V1
SELECT V2
BEGIN
INSERT V3 km=0
UPDATE V1 km=1200
COMMIT

Step-by-Step Walkthrough

The second get("V1") returns the same object from the identity map — no second SELECT.

Changing km on a loaded object is detected at commit by comparing with the snapshot.

V2 was loaded but not changed, so no UPDATE is issued.

All writes are grouped into one transaction (BEGIN…COMMIT).

Common Mistakes

Generic repositories (IRepository<T> with 30 methods) mirroring the ORM — little value, leaky abstraction.

Active Record in complex domains — persistence concerns spread through business rules.

Long-lived units of work — stale identity maps and memory growth.

CQRS everywhere — significant complexity for systems that don't need separate read models.

Trade-offs and Alternatives

Pick per context: Active Record or transaction scripts for simple CRUD; Data Mapper + Repository + Unit of Work (usually via an ORM) for rich domains; CQRS + read models for high read/write asymmetry or complex reporting.

Real-World Use

Entity Framework, Hibernate/JPA, SQLAlchemy sessions, Rails ActiveRecord, Spring Data repositories, CQRS read models in event-sourced systems.

Practice

Q1. (Easy) Which pattern makes an object save itself?

Answer: Active Record.

Q2. (Medium) What two patterns does an EF DbContext implement?

Answer: Unit of Work and Identity Map.

Q3. (Advanced) When is CQRS worth it?

Answer: When read and write workloads differ a lot (complex queries/reporting vs transactional writes), when reads need denormalized views at scale, or when combined with event sourcing; not for straightforward CRUD.

Interview Question

Q1. "Repository vs Data Mapper?" Answer: A Data Mapper moves data between in-memory objects and database rows, keeping them independent — it's the persistence mechanism (often an ORM). A Repository sits above it and gives the domain a collection-like interface for aggregates (orders.byId(id), orders.add(order)), expressed in domain language and hiding query details. Repositories use mappers; mappers don't need repositories.

Quick Quiz

Q1. What does Unit of Work batch?

Answer: All inserts/updates/deletes of a business transaction into one commit.

Q2. What problem does Lazy Load risk?

Answer: N+1 queries.

Q3. What does CQRS separate?

Answer: Command (write) models from query (read) models.

Summary

Enterprise patterns are persistence and application-structure options with trade-offs.

Your ORM likely implements several; know which, and use them deliberately.

Related: Modules 51, 62, 74, 76.