shredbx logo
shredbx shredbx shredbx shredbx Personal
  • Home
  • Lab
  • Portfolio
  • Experience
  • Services
  • Profile
  • Contact
AClaude
  • Home
  • Lab
  • Portfolio
  • Experience
  • Services
  • Profile
  • Contact
Andrei Solovev
Knowledge
Search knowledge... ⌘K
Knowledge · Patterns · Design Patterns

Repository

Collection-like interface mediating between domain and data mapping layers for aggregate persistence

Andrei Solovev

Tags

dddrepositorypersistencedata-access

Overview

Purpose

Collection-like interface mediating between domain and data mapping layers for aggregate persistence

Context

Domain-driven systems where aggregate roots require persistence beyond their
in-memory lifetime. The domain model contains rich business logic that must
remain independent of persistence technology. Multiple consumers (services,
event handlers, scheduled jobs) need to load and store the same aggregate
types. The system may need to support different persistence backends across
environments (relational in production, in-memory in tests).

Problem

How to provide aggregate persistence without coupling the domain model to
data access infrastructure?

Forces:
(1) Domain objects must remain persistence-ignorant — no SQL, no ORM annotations
(2) Data access logic must be encapsulated, not scattered across services
(3) Aggregates must be reconstituted as complete, valid object graphs
(4) Query logic must be expressible without exposing underlying storage details
(5) The persistence mechanism must be substitutable for testing and migration
(6) Only aggregate roots should be directly loadable — internal entities
    are accessed through their root

Solution

Define an interface in the domain layer that mimics an in-memory collection of aggregate roots. The interface exposes operations for adding, removing, and querying aggregates using domain concepts (not storage concepts). A concrete implementation in the infrastructure layer translates these collection operations into actual persistence calls — SQL queries, document store operations, or API calls. The domain layer depends only on the interface. The infrastructure implementation handles object-relational mapping, query translation, and connection management transparently.

One repository per aggregate root. The repository returns fully reconstituted aggregates — never partial objects or raw data structures. Clients interact with the repository as if working with a simple collection: add, remove, find.

Consequences

Benefits: - Persistence ignorance: domain model has zero dependency on storage technology - Encapsulation: all data access logic lives in one place per aggregate - Testability: repository interface can be stubbed with in-memory implementations - Substitutability: swap PostgreSQL for MongoDB by providing a new implementation - Query discipline: domain-level query methods prevent ad-hoc SQL sprawl - Reconstitution guarantee: aggregates are always returned in valid state - Single responsibility: services focus on business logic, not data mapping

Liabilities: - Abstraction overhead: simple CRUD operations gain an additional layer - Query limitation: complex analytical queries may not map well to collection semantics - N+1 risk: naive implementations may issue excessive queries for related aggregates - Repository bloat: accumulation of query methods as requirements grow - Impedance mismatch: collection metaphor can conflict with batch/bulk operations - Transaction boundaries: repository alone does not manage cross-aggregate transactions

Collaborations

Services and event handlers call Repository interface methods using domain concepts (e.g., findByCustomerId, not SELECT * WHERE). The Repository interface lives in the domain layer alongside the AggregateRoot it manages. ConcreteRepository in the infrastructure layer receives these calls, translates domain queries to storage operations, executes them, and reconstitutes full AggregateRoot object graphs from the results. QuerySpecification objects, when used, are interpreted by the ConcreteRepository to build storage-native query expressions. The dependency direction is always inward: infrastructure depends on domain, never the reverse.

Structure

Participants:
- Repository: Interface in domain layer defining collection-like operations
- ConcreteRepository: Infrastructure implementation of Repository interface
- AggregateRoot: Domain object (complete object graph) managed by Repository
- QuerySpecification: Optional domain-level query criteria object

Relationships:
- Repository defines interface for AggregateRoot persistence (abstraction)
- ConcreteRepository implements Repository (realization)
- ConcreteRepository maps AggregateRoot to/from storage format (data mapping)
- QuerySpecification parameterizes Repository query methods (composition)
- Services depend on Repository interface, not ConcreteRepository (dependency inversion)

Layering:
  # Domain layer (MD in FDD)
  Repository[interface]:
    add(aggregate: AggregateRoot): void
    remove(aggregate: AggregateRoot): void
    findById(id: Identity): AggregateRoot
    findAll(spec: QuerySpecification): List<AggregateRoot>

  # Infrastructure layer (SI in FDD)
  SqlRepository implements Repository:
    - Translates to SQL, maps ResultSet to AggregateRoot
  InMemoryRepository implements Repository:
    - Uses Map<Identity, AggregateRoot> for testing

Implementation

In Go (SBX convention):
  1. Define the repository interface in the domain package (pkg/domain/)
     type OrderRepository interface {
         FindByID(ctx context.Context, id OrderID) (*Order, error)
         FindByCustomer(ctx context.Context, cid CustomerID) ([]*Order, error)
         Save(ctx context.Context, order *Order) error
         Delete(ctx context.Context, id OrderID) error
     }

  2. Implement in infrastructure package (internal/persistence/)
     type PostgresOrderRepository struct { db *sql.DB }
     // Methods translate to SQL, handle mapping, return domain objects

  3. Provide in-memory implementation for tests (internal/persistence/memory/)
     type InMemoryOrderRepository struct { store map[OrderID]*Order }

  4. Wire via dependency injection in main.go or DI container
     // Production: repo := persistence.NewPostgresOrderRepository(db)
     // Testing:    repo := memory.NewInMemoryOrderRepository()

Key implementation considerations:
  - Return errors for not-found (do not return nil without error)
  - Use context.Context for cancellation and tracing propagation
  - Repository methods are aggregate-boundary transactions
  - Consider Unit of Work pattern for cross-aggregate transactions
  - Avoid exposing generic query builders — each query is a named method

Example

Consider a ride-sharing application with an Order aggregate containing a root Order entity, a list of LineItems, and a PaymentRecord value object. The domain service needs to load an Order by ID, find all orders for a customer within a date range, and persist state changes after applying business rules. The service currently calls JDBC directly — constructing SQL queries, mapping ResultSets to domain objects, managing transactions, and handling connection pooling. This data access logic is scattered across three service classes, duplicated with slight variations, and tightly couples the domain to PostgreSQL. Switching to a document store for the Order aggregate would require rewriting every service method.

Resolution

Applying the repository pattern to the ride-sharing Order aggregate: an OrderRepository interface is defined in the domain layer with findById, findByCustomerInDateRange, and save methods. A PostgresOrderRepository implementation handles SQL generation, ResultSet-to-Order mapping, and connection pooling — all encapsulated in one infrastructure class. The three service classes now call orderRepository.findById() instead of constructing SQL. Duplication is eliminated. The domain service has zero import of any database package. For testing, an InMemoryOrderRepository returns pre-built Order aggregates without a database. When the team evaluates moving Order storage to MongoDB, only the ConcreteRepository implementation changes — the domain layer and all service tests remain untouched.

Known Uses

  • Spring Data JpaRepository — generic repository interface with derived query methods for JPA entities
  • Rails ActiveRecord — partial repository (combines data mapper and repository; find/where/save on model class)
  • Go database/sql + repository wrapper — idiomatic Go pattern wrapping *sql.DB in domain-typed repository structs
  • Microsoft Entity Framework DbSet<T> — collection-like interface backed by LINQ-to-SQL translation
  • Hibernate Session.get/save/createQuery — ORM-backed repository operations for Java aggregates

See Also

  • aggregate: Repository manages exactly one aggregate root — the aggregate defines the consistency boundary that the repository persists
  • data-mapper: ConcreteRepository typically uses a data mapper internally to translate between domain objects and storage format
  • specification: QuerySpecification encapsulates query criteria as domain objects, enabling composable repository queries
  • unit-of-work: Unit of Work coordinates multiple repository operations within a single transaction boundary
  • adapter: Repository is a domain-specific adapter — it adapts a generic storage interface to a domain-typed collection interface
design structural
shredbx logo shredbx shredbx shredbx shredbx Andrei Solovev

Solution Architect & Lead Software Engineer

ExperiencePortfolioResearch & ExperimentsEducationCertificationSkills
GitHub ↗LinkedIn ↗Email ↗
AVAILABLE FOR NEW PROJECTS
// MY LATEST BEATS
Hobby & Interests

Lab

  • The Lab
  • Framework
  • Components
  • Packages
  • Games
  • Process (SDLC)
  • Knowledge
  • Blog

Andrei

  • Portfolio
  • Experience
  • Services
  • Profile
  • Contact
  • Lifestyle

Team

  • Team
  • Andrei
  • Claude

Legal

  • Privacy
  • Terms
  • Cookies
© 2026 shredbx.com. All rights reserved. — Andrei Solovev |