A Comprehensive Guide to Domain-Driven Design (DDD) with a Practical Folder Structure Example
Crafting a Domain-Driven Design: A Practical Guide with E-commerce Examples
Domain-Driven Design (DDD) is a software development approach that places the primary focus on the business domain and the core business logic, aiming to build a system that truly reflects the complex reality of the business it supports. This approach helps align software architecture with business requirements and promotes a modular, maintainable, and adaptable codebase. This guide covers the principles of DDD, its layered architecture, a practical folder structure, code examples, and the benefits DDD brings to a complex application.
Key Concepts of DDD
Ubiquitous Language: A shared language used by both developers and business stakeholders to describe domain concepts consistently.
Bounded Context: Segregating the domain into distinct boundaries (contexts) to prevent overlap and confusion.
Entities: Objects with a unique identity that persists over time (e.g.,
Customer
).Value Objects: Immutable objects with no identity, representing descriptive aspects (e.g.,
Address
).Aggregates and Aggregate Roots: Collections of related entities and value objects that form a consistent boundary.
Repositories: Interfaces that abstract data access logic, allowing retrieval and storage of aggregates.
Domain Services: Implement business operations that don’t naturally belong to a single entity or value object.
Layered Architecture in DDD
DDD is typically implemented with a layered architecture, where each layer has a specific responsibility. This separation allows the application to be modular and easier to maintain.
Domain Layer: Contains core business logic, domain entities, value objects, and domain services.
Application Layer: Defines specific use cases and workflows, coordinating between the domain and other layers.
Infrastructure Layer: Handles technical details such as database access, external APIs, and file systems.
Presentation Layer: Manages user interactions and external interfaces, exposing the application’s functionality.
Folder Structure for DDD (With E-commerce Example)
A sample folder structure for an e-commerce application, organized according to DDD principles:
ecommerce-app/
├── src/
│ ├── domain/
│ │ ├── customers/
│ │ │ ├── Customer.ts # Customer entity
│ │ │ ├── Address.ts # Address value object
│ │ │ ├── CustomerRepository.ts # Repository interface
│ │ ├── orders/
│ │ │ ├── Order.ts # Order entity
│ │ │ ├── OrderLine.ts # OrderLine entity
│ │ │ ├── OrderRepository.ts # Repository interface
│ │ └── products/
│ │ ├── Product.ts # Product entity
│ │ ├── ProductRepository.ts # Repository interface
│ ├── application/
│ │ ├── RegisterCustomerUseCase.ts # Use case for registering a customer
│ │ ├── PlaceOrderUseCase.ts # Use case for placing an order
│ ├── infrastructure/
│ │ ├── database/
│ │ │ ├── CustomerRepositoryImpl.ts # Implementation of customer repository
│ │ │ ├── OrderRepositoryImpl.ts # Implementation of order repository
│ │ ├── http/
│ │ └── files/
│ ├── presentation/
│ │ ├── controllers/
│ │ │ ├── CustomerController.ts # Handles customer-related HTTP requests
│ │ │ └── OrderController.ts # Handles order-related HTTP requests
│ │ ├── views/
│ │ │ ├── CustomerView.tsx # UI for displaying customer info
│ │ │ └── OrderView.tsx # UI for displaying order info
│ │ └── routes/
│ │ └── index.ts # Routes configuration
│ └── shared/
└── tests/
Code Implementation
1. Domain Layer
The Domain Layer is the core of the application, containing the essential business logic, entities, and repositories.
Example: Customer Entity
In an e-commerce app, a Customer
entity may contain personal details and domain logic specific to customers.
// src/domain/customers/Customer.ts
export class Customer {
constructor(
public id: string,
public name: string,
public email: string
) {}
public updateEmail(newEmail: string): void {
this.email = newEmail;
}
}
Address Value Object
A Value Object
does not have an identity and is used to describe an entity. Here’s an example of an Address
value object:
// src/domain/customers/Address.ts
export class Address {
constructor(
public street: string,
public city: string,
public zipCode: string
) {}
public toString(): string {
return `${this.street}, ${this.city}, ${this.zipCode}`;
}
}
Customer Repository Interface
Repositories abstract data access, making it possible to swap out database implementations without affecting the domain logic.
// src/domain/customers/CustomerRepository.ts
import { Customer } from './Customer';
export interface CustomerRepository {
findById(id: string): Promise<Customer | null>;
save(customer: Customer): Promise<void>;
}
2. Application Layer
The Application Layer defines use cases that orchestrate domain logic.
Example: RegisterCustomerUseCase
A RegisterCustomerUseCase
coordinates the customer registration process.
// src/application/RegisterCustomerUseCase.ts
import { Customer } from '../domain/customers/Customer';
import { CustomerRepository } from '../domain/customers/CustomerRepository';
export class RegisterCustomerUseCase {
constructor(private customerRepository: CustomerRepository) {}
public async execute(name: string, email: string): Promise<void> {
const customer = new Customer(/* generate unique ID */, name, email);
await this.customerRepository.save(customer);
}
}
3. Infrastructure Layer
The Infrastructure Layer provides technical implementation, such as interacting with databases, external APIs, and other infrastructure resources.
Example: CustomerRepository Implementation
This class implements the data access methods defined in the repository interface.
// src/infrastructure/database/CustomerRepositoryImpl.ts
import { Customer } from '../../domain/customers/Customer';
import { CustomerRepository } from '../../domain/customers/CustomerRepository';
export class CustomerRepositoryImpl implements CustomerRepository {
public async findById(id: string): Promise<Customer | null> {
// Logic to find and return customer by ID
}
public async save(customer: Customer): Promise<void> {
// Logic to save customer data to the database
}
}
4. Presentation Layer
The Presentation Layer manages user interactions and external interfaces. It includes controllers for handling requests and views for rendering information to users.
Example: CustomerController
A CustomerController
manages customer-related HTTP requests, delegating the actual work to use cases.
// src/presentation/controllers/CustomerController.ts
import { RegisterCustomerUseCase } from '../../application/RegisterCustomerUseCase';
export class CustomerController {
constructor(private registerCustomerUseCase: RegisterCustomerUseCase) {}
public async register(req, res): Promise<void> {
const { name, email } = req.body;
try {
await this.registerCustomerUseCase.execute(name, email);
res.status(201).json({ message: 'Customer registered successfully' });
} catch (error) {
res.status(400).json({ error: error.message });
}
}
}
Routes Configuration
Routes link URL paths to controller actions, defining API endpoints.
// src/presentation/routes/index.ts
import express from 'express';
import { CustomerController } from '../controllers/CustomerController';
const router = express.Router();
const customerController = new CustomerController();
router.post('/customers/register', (req, res) => customerController.register(req, res));
export default router;
Customer View (React Component)
In applications with a frontend, the Presentation Layer may include user-facing components like this CustomerView
.
// src/presentation/views/CustomerView.tsx
import React from 'react';
export const CustomerView = ({ customer }) => (
<div>
<h1>Customer Information</h1>
<p>Name: {customer.name}</p>
<p>Email: {customer.email}</p>
</div>
);
Benefits of DDD
Separation of Concerns: Each layer has a dedicated responsibility, reducing coupling and making the codebase easier to understand and maintain.
Modularity: Independent layers make it possible to scale different aspects of the application, add features, and swap out dependencies without major restructuring.
Testability: Each layer can be tested in isolation, promoting easier testing of complex business logic and application flow.
Adaptability: DDD makes it easy to adapt to changing business requirements, as the architecture is aligned with domain concepts and bounded contexts.
Alignment with Business Goals: By modeling core business concepts directly in code, DDD ensures that the application is closely aligned with business objectives.