DomainDrivenJSDomainDrivenJS
Home
  • Getting Started
  • Quick Start
  • DDD Fundamentals
  • Core Concepts
  • Advanced Topics
API
Examples
GitHub
Home
  • Getting Started
  • Quick Start
  • DDD Fundamentals
  • Core Concepts
  • Advanced Topics
API
Examples
GitHub
  • Introduction

    • Getting Started with DomainDrivenJS
    • Quick Start Guide
  • DDD Fundamentals

    • Introduction to Domain-Driven Design
    • Strategic Design in Domain-Driven Design
    • Tactical Design in Domain-Driven Design
    • Ubiquitous Language
  • Core Concepts

    • Understanding Value Objects
    • Working with Entities
    • Working with Aggregates
    • Working with Repositories
    • Working with Domain Events
    • Working with Specifications
    • Working with Domain Services
  • Advanced Topics

    • Extending DomainDrivenJS Components
    • Testing Domain-Driven Design Applications
    • Domain-Driven Design Best Practices
    • Domain-Driven Design Anti-Patterns

Working with Repositories

Repositories are a critical pattern in Domain-Driven Design that provides a clean separation between your domain model and your data storage. They abstract away the details of how objects are persisted and retrieved.

What is a Repository?

A repository is a collection-like interface that mediates between the domain model and data mapping layers, providing an illusion of an in-memory collection of domain objects. Think of it as a specialized "bookshelf" where your domain objects are stored and retrieved.

Real-world Analogy

Think of a library. When you want a book, you don't need to know how the library organizes its shelves, its cataloging system, or where specific books are physically located. You simply ask the librarian for a book by title or author. The librarian (the repository) handles all the details of finding, retrieving, and returning the book to its proper place. Similarly, repositories in code hide all the complex details of data storage and retrieval, allowing the rest of your application to work with domain objects directly without concerning itself with how or where they're stored.

Key characteristics:

  • Provides a collection-like interface for accessing domain objects
  • Abstracts away data storage and retrieval mechanisms
  • Mediates between the domain and data mapping layers
  • Enables testability and flexibility in your domain model

Why Use Repositories?

Repositories offer several benefits:

  • Separation of concerns: Your domain logic remains pure and focused, without being entangled with database access code
  • Improved testability: Easily swap real storage with in-memory implementations for testing
  • Simplified domain code: Domain logic works with repositories, not data access mechanisms
  • Storage flexibility: Change database technologies without affecting domain code
  • Query optimization: Repositories can optimize queries based on specific storage technologies
  • Domain focus: Repositories speak the language of the domain, not the language of the database

How Repositories Work

Repositories act as a boundary between two very different worlds:

┌─────────────────────┐     ┌───────────────┐     ┌─────────────────────┐
│                     │     │               │     │                     │
│   Domain Model      │◄────┤  Repository   ├────►│   Data Storage      │
│  (Entities, etc.)   │     │               │     │  (SQL, NoSQL, etc.) │
│                     │     │               │     │                     │
└─────────────────────┘     └───────────────┘     └─────────────────────┘

On one side, the repository accepts and returns domain objects (entities, aggregates). On the other side, it translates these into the data format required by your storage technology.

When a repository saves an object:

  1. It receives a domain object
  2. It maps the object to the storage format (e.g., SQL table rows, document JSON)
  3. It uses the appropriate storage mechanism to persist the data

When a repository retrieves an object:

  1. It queries the storage mechanism
  2. It maps the raw data back into domain objects
  3. It returns fully reconstituted domain objects

Creating Repositories with DomainDrivenJS

DomainDrivenJS provides a straightforward way to create repositories:

import { z } from 'zod';
import { entity, repository } from 'domaindrivenjs';

// First, let's define a simple Product entity
const Product = entity({
  name: 'Product',
  schema: z.object({
    id: z.string().uuid(),
    name: z.string().min(1),
    price: z.number().positive(),
    stockLevel: z.number().int().nonnegative()
  }),
  identity: 'id'
});

// Now, let's create a repository for our Product entity
const ProductRepository = repository({
  name: 'ProductRepository',
  entity: Product,
  methods: {
    async findByName(name) {
      // This will be implemented by the adapter
      return this.findOne({ name });
    },
    async findInStock() {
      // This will be implemented by the adapter
      return this.findMany({ stockLevel: { $gt: 0 } });
    },
    async updateStock(id, newStockLevel) {
      // This will be implemented by the adapter
      return this.update(id, { stockLevel: newStockLevel });
    }
  }
});

Let's break down the components:

  1. name: A descriptive name for your repository
  2. entity: The entity type this repository will manage
  3. methods: Custom query and operation methods specific to this repository

Repository Adapters

Real-world Analogy

Repository adapters are like power adapters for international travel. Whether you're in Europe, Asia, or America with different wall outlets, the adapter ensures your device gets the power it needs. Similarly, repository adapters ensure your domain objects work with different storage systems (MongoDB, SQL, memory) without changing your core code.

A key strength of the repository pattern is its abstraction of storage details through adapters. DomainDrivenJS provides adapters for different storage systems:

import { InMemoryAdapter, MongoAdapter, SqliteAdapter } from 'domaindrivenjs/adapters';

// In-memory adapter (great for testing)
const inMemoryProductRepo = ProductRepository.create(
  new InMemoryAdapter()
);

// MongoDB adapter
const mongoProductRepo = ProductRepository.create(
  new MongoAdapter({
    connectionString: 'mongodb://localhost:27017',
    database: 'my-shop',
    collection: 'products'
  })
);

// SQLite adapter
const sqliteProductRepo = ProductRepository.create(
  new SqliteAdapter({
    filename: './my-shop.db',
    table: 'products'
  })
);

Each adapter implements the same interface but handles the specific details of its storage technology. This allows you to switch storage technologies with minimal code changes.

Using Repositories

Once you've connected your repository to an adapter, you can use it to work with your entities:

// Create a new product
const newProduct = await productRepo.save(
  Product.create({
    id: '123e4567-e89b-12d3-a456-426614174000',
    name: 'Mechanical Keyboard',
    price: 89.99,
    stockLevel: 50
  })
);

// Find a product by ID
const product = await productRepo.findById('123e4567-e89b-12d3-a456-426614174000');

// Find products by criteria
const inStockProducts = await productRepo.findInStock();
const keyboardProducts = await productRepo.findMany({ name: { $contains: 'Keyboard' } });

// Update a product
await productRepo.updateStock('123e4567-e89b-12d3-a456-426614174000', 45);

// Delete a product
await productRepo.delete('123e4567-e89b-12d3-a456-426614174000');

Standard Repository Methods

Real-world Analogy

Repository methods are like the standard services offered by a storage facility. You can store items (save), retrieve them (find), check if you have something in storage (exists), replace items (update), or remove them altogether (delete)—all without needing to know how the facility is organized internally.

All DomainDrivenJS repositories come with these standard methods:

MethodDescription
findById(id)Find an entity by its identifier
findOne(criteria)Find a single entity matching criteria
findMany(criteria)Find all entities matching criteria
exists(id)Check if an entity with the given ID exists
save(entity)Create or update an entity
update(id, changes)Update an entity by ID with partial changes
delete(id)Delete an entity by ID
count(criteria)Count entities matching criteria

Query Criteria

DomainDrivenJS repositories support a flexible query criteria syntax:

// Basic equality
await productRepo.findMany({ name: 'Mechanical Keyboard' });

// Comparison operators
await productRepo.findMany({ price: { $lt: 100 } });
await productRepo.findMany({ stockLevel: { $gte: 10 } });

// Logical operators
await productRepo.findMany({
  $or: [
    { name: { $contains: 'Keyboard' } },
    { name: { $contains: 'Mouse' } }
  ],
  price: { $lt: 200 }
});

// String operations
await productRepo.findMany({ name: { $startsWith: 'Mech' } });
await productRepo.findMany({ description: { $contains: 'ergonomic' } });

Advanced Repository Patterns

Specialized Finders

You can create specialized finder methods for common queries:

const ProductRepository = repository({
  name: 'ProductRepository',
  entity: Product,
  methods: {
    async findByCategory(categoryId) {
      return this.findMany({ categoryId });
    },
    async findBestSellers() {
      return this.findMany(
        { salesRank: { $lte: 100 } },
        { sort: { salesRank: 'asc' }, limit: 10 }
      );
    }
  }
});

Transaction Support

Repositories can support transactions to ensure data consistency:

// Using the transaction manager from your adapter
const { transactionManager } = mongoAdapter;

await transactionManager.runInTransaction(async (session) => {
  // Pass the session to your repository operations
  await productRepo.updateStock('product-1', 45, { session });
  await orderRepo.save(newOrder, { session });
});

Batch Operations

For performance, you can perform batch operations:

// Batch insert
await productRepo.saveMany([product1, product2, product3]);

// Batch update
await productRepo.updateMany(
  { category: 'keyboards' },
  { inStock: false }
);

// Batch delete
await productRepo.deleteMany({ expiryDate: { $lt: new Date() } });

Working with Specifications

Repositories can work seamlessly with specifications (see Specifications):

// Create a specification
const InStockSpec = specification({
  name: 'InStock',
  isSatisfiedBy: product => product.stockLevel > 0,
  toQuery: () => ({ stockLevel: { $gt: 0 } })
});

// Use it with a repository
const inStockProducts = await productRepo.findMany(InStockSpec);

// Combine specifications
const FeaturedAndInStock = FeaturedSpec.and(InStockSpec);
const featuredInStockProducts = await productRepo.findMany(FeaturedAndInStock);

Repository Composition

You can compose repositories for more complex operations:

const OrderService = {
  async placeOrder(cart, customer, productRepo, orderRepo) {
    // Verify all products are in stock
    for (const item of cart.items) {
      const product = await productRepo.findById(item.productId);
      if (!product || product.stockLevel < item.quantity) {
        throw new Error(`Product ${item.productId} not available in requested quantity`);
      }
    }
    
    // Create the order
    const order = Order.create({
      id: generateId(),
      customerId: customer.id,
      items: cart.items,
      status: 'PLACED',
      placedAt: new Date()
    });
    
    // Update product stock
    for (const item of cart.items) {
      await productRepo.updateStock(
        item.productId, 
        (await productRepo.findById(item.productId)).stockLevel - item.quantity
      );
    }
    
    // Save the order
    return orderRepo.save(order);
  }
};

Testing with Repositories

In-memory adapters make testing with repositories simple:

import { InMemoryAdapter } from 'domaindrivenjs/adapters';

describe('ProductService', () => {
  let productRepo;
  
  beforeEach(() => {
    // Create a fresh in-memory repository for each test
    productRepo = ProductRepository.create(new InMemoryAdapter());
  });
  
  test('discounting products decreases their price', async () => {
    // Arrange
    const product = Product.create({
      id: '123',
      name: 'Test Product',
      price: 100,
      stockLevel: 10
    });
    await productRepo.save(product);
    
    // Act
    await ProductService.applyDiscount(productRepo, '123', 0.1);
    
    // Assert
    const updatedProduct = await productRepo.findById('123');
    expect(updatedProduct.price).toBe(90);
  });
});

Common Pitfalls

  1. Repository per table: Creating repositories that match database tables instead of aggregates
  2. Leaking persistence concerns: Exposing storage-specific details in the repository interface
  3. Anemic repositories: Not providing domain-specific query methods, just basic CRUD
  4. Fat repositories: Adding business logic that belongs in domain services
  5. Inconsistent transaction boundaries: Not considering aggregate boundaries when designing transactions

Best Practices

Real-world Analogy

Good repository design is like a well-organized kitchen. Ingredients (data) are stored logically (separate repositories for different types), the chef (domain service) requests ingredients as needed, and kitchen staff (repositories) know exactly where to find and how to prepare each ingredient. No one needs to know that milk is on the third shelf of the walk-in cooler—they just ask for milk.

  1. Repository per aggregate: Create one repository for each aggregate root, not for every entity
  2. Keep repositories focused: Each repository should handle one type of entity
  3. Abstract storage details: Don't expose storage-specific code through repositories
  4. Use dependency injection: Pass repositories to services that need them
  5. Optimize for common queries: Add custom methods for frequently used queries
  6. Consider caching: Implement caching strategies for performance-critical repositories
  7. Respect aggregate boundaries: Repositories should enforce the consistency boundaries of aggregates

Next Steps

Now that you understand repositories, you might want to learn about:

  • Specifications - Encapsulate query and validation rules
  • Domain Events - Capture significant changes in your domain
  • Testing Repositories - Advanced techniques for testing repositories
Help us improve this page!
Last Updated:: 4/22/25, 11:22 AM
Contributors: Marco Müllner
Prev
Working with Aggregates
Next
Working with Domain Events