A Comprehensive Guide to Domain-Driven Design (DDD) with a Practical Folder Structure Example
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.
Conclusion
Subscribe to my newsletter
Read articles from ByteScrum Technologies directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
ByteScrum Technologies
ByteScrum Technologies
Our company comprises seasoned professionals, each an expert in their field. Customer satisfaction is our top priority, exceeding clients' needs. We ensure competitive pricing and quality in web and mobile development without compromise.