Repository Pattern — Basic
Decouple business logic from database engines and storage details. Master Martin Fowler's collection-like mediator pattern, maintain safe parameterized queries, establish clear architectural boundaries between Services and Repositories, and refactor a messy backend application interactively.
Core Architectural Rule: A Repository is an in-memory collection abstraction that mediates between domain/application logic and physical data persistence. It is NOT merely a file storing raw SQL strings, and it is NOT mandatory for every simple CRUD app. The service orchestrates business workflows; the repository retrieves and stores entities.
What is the Repository Pattern?
Understand how repositories act as a collection-like illusion for data persistence, decoupling domain concepts from database storage mechanics.
The Collection-Like Illusion
In his classic work Patterns of Enterprise Application Architecture (PoEAA), Martin Fowler defined the Repository pattern as an abstraction that:
"Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects."
To the Service Layer, the repository feels like an in-memory list or array of objects: you can add(entity), get(id), filter(criteria), and remove(entity). The service has no idea whether records come from PostgreSQL, MongoDB, Redis, or an in-memory test stub.
Repository vs Database vs Service
Beginners frequently confuse these three components. Here is how they cleanly differ:
- Database: The external, physical persistence storage system (e.g., PostgreSQL disk tables, connection pools, binary indexes).
- Repository: The application-level code module that speaks SQL or ORM dialect, executes queries safely, maps table rows to domain objects, and hides persistence mechanics.
- Service: The business coordinator. Evaluates business rules, computes prices, validates eligibility, and orchestrates calls to one or more repositories.
Why Use a Repository? (Problems, Benefits & Trade-Offs)
See what happens when a service directly embeds raw database queries, and weigh the architectural trade-offs before introducing this abstraction.
The Problem: Database-Polluted Service
When a Service manages database connections and raw queries directly, several architectural problems emerge:
- Hard to test: You cannot test discount calculations without booting a live database.
- Duplicated queries: Other services needing user lookup duplicate the same SQL string.
- Schema lock-in: If the table column changes from
user_idtocustomer_id, business code breaks.
The Solution: Dedicated Repository
By extracting data operations into an OrderRepository and UserRepository, the service focuses solely on business logic:
- Fast unit tests: Mock the repository with a JavaScript array; test in 2ms.
- Centralized queries: SQL queries live in one place; update schema in one file.
- Swappable engines: Switch from raw PostgreSQL to Prisma or SQLite without touching business rules.
Architectural Trade-Offs & When to Skip the Pattern
The Repository Pattern is a tool, not a religious mandate. Before adding repository files for every database table, consider:
Creating an interface, repository implementation, and service wrapper for a simple table with 3 fields adds ceremony without benefit.
If an endpoint just reads records and returns them with zero business logic, calling an ORM directly from a controller is often pragmatic and sufficient.
Building a monolithic repository that does complex joins across 15 unrelated tables creates tight coupling and defeats the pattern.
Repository Responsibilities & Boundaries
Define precise architectural boundaries. Know what belongs inside a repository and what must stay out.
What BELONGS in a Repository
- Standard Collection Operations:
findById(id),findAll(filters),create(entity),update(id, data),delete(id). - Domain Queries: Semantic retrieval methods like
findActiveByCustomerId(customerId),findByEmail(email),findPendingOrders(). - Safe Parameterized SQL: Using query placeholders (
$1, $2in Postgres,?in MySQL) to prevent SQL injection. - Data Mapping / Hydration: Translating database snake_case columns (
created_at_epoch) into clean domain model objects (createdAt). - Database Error Translation: Catching driver error codes (e.g., unique key violation
23505) and throwing understandable domain persistence errors.
What DOES NOT Belong in a Repository
- HTTP Concerns: Never pass Express
req,res, headers, cookies, or HTTP status codes into a repository. - Authentication & Session Tokens: Decrypting JWTs or verifying session cookies belongs in middleware/controllers.
- Business Logic & Calculations: Calculating discounts, shipping fees, tax rates, or evaluating user tiers belongs in the Service Layer.
- External Side-Effects: Sending confirmation emails, processing Stripe credit cards, or dispatching WebSockets.
- Leaky SQL Queries: Never let controllers or services pass raw SQL clauses (e.g.,
repo.findWhere("status = 'PAID'")).
Service vs Repository (Side-by-Side Comparison)
Examine how the Service Layer and Repository Layer collaborate during a realistic e-commerce checkout flow.
| Dimension | Service Layer | Repository Layer |
|---|---|---|
| Primary Question | "What business rules apply to this operation?" | "How do I retrieve or store this domain entity?" |
| Knows About | Business rules, workflows, calculations, repositories, email dispatchers | Database drivers, connection pools, table schemas, SQL queries, row mapping |
| Input & Output | Domain DTOs, business command parameters → returns business result | Entity IDs, query criteria → returns Domain Entities or null |
| Unit Testing | Mock the repositories with in-memory arrays; tests run in milliseconds | Integration tests against an ephemeral database (Docker/SQLite) to verify SQL |
| Reusable By | REST Controllers, GraphQL Resolvers, CLI commands, Kafka message consumers | Multiple services across the application needing data access |
Collaboration in Action: E-Commerce Checkout Flow
Notice how the CheckoutService handles all decisions while Repositories handle all storage:
Real Implementations: Express.js & FastAPI
See modern production implementations in Node.js (Express) and Python (FastAPI with Dependency Injection).
Interactive Refactoring Workbench & Simulator
Hands-on coding exercise: Extract direct database queries from orderService.js into orderRepository.js. Run verification tests and send simulated HTTP requests to verify complete decoupling.
Production Debugging: 7 Architectural Anti-Patterns
Identify and fix real-world architectural bugs where data access responsibilities leak across layers.
Scenario #1: Raw SQL Queries Embedded Directly Inside Service Layer
Mini Challenge & 5 Golden Architectural Rules
Test your intuition: classify realistic backend duties into Controller, Service, or Repository layers.
Responsibility Boundary Matrix
Assign each backend task to its correct architectural layer:
5 Golden Rules of the Repository Pattern
A repository exposes collection-like methods (findById, create, delete). It hides table columns and database connection pools from higher layers.
Repositories must never accept Express req/res or FastAPI HTTP objects. They are transport-agnostic and return pure domain entities or DTOs.
Discounts, tax formulas, user tier eligibility, and workflow logic belong strictly in the Service Layer. Repositories only fetch and save data.
All SQL queries inside repositories must use safe parameters ($1, $2) to prevent SQL injection vulnerabilities. Never concatenate raw strings.
Do not introduce repositories for trivial CRUD endpoints without business rules. Use the pattern when domain complexity or testing requirements justify it.
Mastery Assessment Quiz (7 Concept Questions)
Verify your deep understanding of the Repository Pattern, data-access abstractions, and architectural boundaries.