A Comprehensive Guide to Domain-Driven Design (DDD) with a Practical Folder Structure Example

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

  1. Ubiquitous Language: A shared language used by both developers and business stakeholders to describe domain concepts consistently.

  2. Bounded Context: Segregating the domain into distinct boundaries (contexts) to prevent overlap and confusion.

  3. Entities: Objects with a unique identity that persists over time (e.g., Customer).

  4. Value Objects: Immutable objects with no identity, representing descriptive aspects (e.g., Address).

  5. Aggregates and Aggregate Roots: Collections of related entities and value objects that form a consistent boundary.

  6. Repositories: Interfaces that abstract data access logic, allowing retrieval and storage of aggregates.

  7. 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.

  1. Domain Layer: Contains core business logic, domain entities, value objects, and domain services.

  2. Application Layer: Defines specific use cases and workflows, coordinating between the domain and other layers.

  3. Infrastructure Layer: Handles technical details such as database access, external APIs, and file systems.

  4. 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

  1. Separation of Concerns: Each layer has a dedicated responsibility, reducing coupling and making the codebase easier to understand and maintain.

  2. Modularity: Independent layers make it possible to scale different aspects of the application, add features, and swap out dependencies without major restructuring.

  3. Testability: Each layer can be tested in isolation, promoting easier testing of complex business logic and application flow.

  4. Adaptability: DDD makes it easy to adapt to changing business requirements, as the architecture is aligned with domain concepts and bounded contexts.

  5. Alignment with Business Goals: By modeling core business concepts directly in code, DDD ensures that the application is closely aligned with business objectives.

Conclusion
In summary, Domain-Driven Design provides a robust framework for developing complex applications with a clear structure and focus on domain logic. For complex domains like e-commerce, DDD not only helps in organizing code but also in delivering a maintainable, scalable, and flexible architecture.