Domain Driven Design

From A Technological Wiki
Jump to navigation Jump to search

Domain Driven Design is a major software design approach, focusing on modeling software to match a domain according to input from that domain's experts.

Under domain-driven design, the structure and language of software code (class names, class methods, class variables) should match the business domain.

Domain-driven design is predicated on the following goals:

  • placing the project's primary focus on the core domain and domain logic;
  • basing complex designs on a model of the domain;
  • initiating a creative collaboration between technical and domain experts to iteratively refine a conceptual model that addresses particular domain problems.

When trying to control the domain model, this problems may be at hand:

  • The team has tried to combine all domain concepts and knowledge in on domain model.
  • Two vaguely related concepts may be represented as one domain model.
  • Regarding data, having one large database imply that developers can take that information from different points without taking into account business data rules.

With bounded contexts we make sure that domain models can't directly make use of domain models from other contexts by making them physically bounded.

There are several key concepts and practices in DDD:

Ubiquitous Language

Ubiquitous Language is an essential practice in DDD. It means that both the domain experts (people who understand the business) and developers use the same terms and phrases when discussing the system. This shared language should be reflected in the code itself.

For instance, if domain experts use the term "backorder" instead of "out of stock", then your domain model (and the associated code) should also use "backorder".

Bounded Contexts

As systems grow, it's often impractical to have a single unified model for everything. Different parts of a large system might have different models for what seems to be the same thing.

A Bounded Context is a boundary within which a particular model is defined and applicable. Different bounded contexts can have different models for what appears, on the surface, to be the same domain concept.

For example, the sales context might view a Product differently from the shipping context. In sales, pricing and descriptions matter. In shipping, weight and dimensions might be more critical.

In practical terms, this might mean separate sets of entities, value objects, and aggregates for different parts of your application, even if they seemingly relate to the same "thing".

Entities

An Entity is an object that is defined by its identity rather than its attributes. This means two entities can have identical attributes, but they are considered distinct if their identities are different.

Characteristics of an Entity

Identity: An entity possesses a unique identity that runs through time and different states. This is typically represented with an ID. No two entities should share the same ID within the same bounded context.

Mutability: Unlike value objects, entities can change over time. However, their identity remains constant.

Encapsulation: An entity contains both data (attributes) and behavior (methods or operations). It's a good practice for entities to be responsible for their own behavior based on their data.

Lifespan: Entities usually have a lifecycle during which they undergo various state changes.

Entity Example in PHP

Let's consider a User entity for a clearer understanding:

class User {
    private $userId;
    private $email;
    private $password;

    public function __construct($userId, $email, $password) {
        $this->userId = $userId;
        $this->email = $email;
        $this->password = $password;
    }

    public function setEmail($newEmail) {
        $this->email = $newEmail;
    }

    public function getUserId() {
        return $this->userId;
    }

    // ... more getters, setters, and other methods.
}

In this User entity example:

  • Identity: The $userId represents the unique identity. This ID will remain constant even if the user's email or password changes.
  • Mutability: We can see that the User entity can undergo changes, like changing the email or password.
  • Encapsulation: The entity encapsulates its own data and behavior like the setEmail method which act on the internal data.

Design Considerations

  • Consistency: Ensure that changes to the entity keep it in a consistent state. Often, this means applying domain rules or invariants.
  • State Changes Through Methods: Instead of exposing internal attributes and changing them directly, provide methods to perform the changes. This allows the entity to enforce rules and invariants.
  • Avoid Anemic Entities: An "anemic" entity is one that separates data and behavior, essentially becoming just a bag of getters and setters. Aim for rich entities where behavior and data are closely intertwined.
  • Identity Management: Be cautious about how the entity's identity is generated and assigned. In many cases, using a database to generate unique IDs is convenient, but sometimes domain-specific identity strategies might be required.

Entities represent the core operational units in a domain, each with a distinct lifecycle and identity. Properly designed entities ensure the model's consistency and expressiveness, playing a crucial role in implementing the domain's business logic.

Value Objects

These are objects that describe aspects of the domain with no conceptual identity. Their equality is based on their attribute values.

Characteristics of a Value Object

  • Immutability: A key feature of value objects is that they are immutable. This means once they are created, their state cannot change. Any operation that appears to change the state should return a new Value Object instead.
  • Equality: Value Objects are equal if all their attribute values are equal. This is different from entities, which are equal based on their identifiers.
  • No Side Effects: Operations on Value Objects should have no side effects on external states.
  • Self-validation: Value Objects should always be in a valid state. This means any operation that would result in an invalid state should be prohibited.
class Money {
    private $amount;
    private $currency;

    public function __construct($amount, $currency) {
        $this->amount = $amount;
        $this->currency = $currency;
    }

    // ... other methods, like adding or subtracting money, currency conversion, etc.
}

When to use Value Objects

  • When you need to model a domain concept that does not have a conceptual identity.
  • When immutability is beneficial. Since Value Objects can't change after they're created, they're inherently thread-safe and free from side effects.
  • When encapsulating logic and behavior around specific descriptive aspects of your domain. Value Objects are powerful tools in domain modeling, allowing for rich behavior and business logic to be encapsulated in small, reusable, and immutable objects. They make the model expressive and aligned closely with the business domain.

Repositories

Repositories are responsible for providing an object-oriented view of the data store (like a database) for the aggregates they serve. An aggregate is a cluster of domain objects (entities and value objects) treated as a single unit for data changes. Each aggregate has a root, which is the only member of the aggregate that outside objects are allowed to hold references to.

Repositories essentially act as an in-memory collection of aggregate roots. From the domain model's perspective, aggregates are retrieved from and persisted to this collection.

Responsibilities of a Repository

  • Retrieving Aggregates: A repository will provide methods to retrieve aggregates based on certain criteria. Common methods include findById, findAll, findBySomeCriteria, etc.
  • Saving Aggregates: Repositories manage the persistence of aggregates. This includes both saving new aggregates and updating existing ones.
  • Deleting Aggregates: Repositories can also manage the deletion of aggregates from the data store.
  • Querying: Repositories often provide query methods to retrieve specific subsets of aggregates or to perform searches.

Principles of Repositories

  • Interface and Implementation Separation: Repositories should have a clear interface that the domain model relies on. The actual implementation (e.g., whether the data is stored in a relational database, NoSQL database, or even an in-memory store) should be separate from this interface.
  • No Business Logic: Repositories are purely for data access. Any business logic should reside in domain entities, value objects, or domain services.
  • Aggregate Root Focus: A repository should be centered around an aggregate root. This ensures the consistency and integrity of the aggregate's data.

Example in PHP

Consider an Order aggregate root. We'll define a repository interface for it:

interface OrderRepository {
    public function findById(int $id): ?Order;
    public function findAll(): array;
    public function save(Order $order): void;
    public function remove(Order $order): void;
}

Now, a simple implementation using PDO for a MySQL database might look something like:

class MySqlOrderRepository implements OrderRepository {
    private $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function findById(int $id): ?Order {
        $stmt = $this->pdo->prepare('SELECT * FROM orders WHERE id = :id');
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch();

        if (!$row) return null;

        // Convert row to Order entity (pseudo-code, actual mapping can be complex)
        return new Order($row['id'], $row['some_field'], ...);
    }

    public function findAll(): array {
        // Similar implementation, but fetch all rows and convert them
    }

    public function save(Order $order): void {
        // Depending on whether the order is a new one or an existing one,
        // perform an INSERT or UPDATE operation
    }

    public function remove(Order $order): void {
        // Execute a DELETE operation on the database
    }
}

Advantages of Using Repositories

  • Separation of Concerns: Domain logic remains separate from data access logic.
  • Flexibility: The data store can be changed without impacting the domain model.
  • Testability: With the separation of the repository interface and its implementation, it's easier to write tests by mocking the repository. While the repository pattern offers several benefits, it's essential to be mindful of potential pitfalls, such as over-fetching data or introducing inefficiencies due to not leveraging database-specific features when needed.

Domain Events

Domain events are a crucial concept in Domain-Driven Design. They represent something that has happened in the domain, a change in the state of the system, which is of interest to other parts of the system or even to external systems.

Characteristics of Domain Events

Immutability: Once an event is created, it should not be altered. This ensures consistency and reliability in event processing. Information-rich: An event typically contains all the information related to the change. This often includes data about the change itself, as well as metadata like the event creation time or the source of the event. Named in the Past Tense: Domain events describe something that has already occurred, so they are usually named in the past tense (e.g., OrderPlaced, ProductShipped).

Advantages of Domain Events

  • Decoupling: Events help in decoupling different parts of a system. For instance, rather than the order management component directly telling the shipping component to ship an item (tight coupling), it can just raise an OrderPlaced event. The shipping component can then listen to this event and act upon it.
  • Interoperability: Events can be used to integrate with external systems without having to couple your core domain logic to external APIs.
  • Historical Record: If stored, events can serve as a historical record or audit log of all changes in the system.
  • Alternative State Representation: Event Sourcing is a pattern where the state of the system is derived by replaying events. This contrasts with the typical CRUD pattern where the state is stored directly.

Implementation in PHP

Let's look at a simple example in PHP.

Defining an Event:

class OrderPlaced {
    private $orderId;
    private $orderDate;

    public function __construct($orderId, \DateTime $orderDate) {
        $this->orderId = $orderId;
        $this->orderDate = $orderDate;
    }

    public function getOrderId() {
        return $this->orderId;
    }

    public function getOrderDate() {
        return $this->orderDate;
    }
}

For raising an event, within your aggregate root or domain entity, you might have a method that does something significant (like placing an order). This method will also raise the event.

class Order {
    // ... other properties and methods

    public function place() {
        // ... logic to place the order
        $event = new OrderPlaced($this->id, new \DateTime());
        // Dispatch the event
        EventDispatcher::dispatch($event);
    }
}

For handling an event, an event listener would handle the action required when an event occurs.

class NotifyCustomerOnOrderPlaced {
    public function handle(OrderPlaced $event) {
        // Logic to notify the customer, maybe send an email or SMS.
    }
}

The EventDispatcher would be responsible for delivering the event to the appropriate listeners. There are many event dispatcher libraries available in PHP (e.g., Symfony's EventDispatcher component) that can help manage this process.

Domain events provide a way to communicate changes in your system in a decoupled manner, promoting cleaner architecture, easier integration, and richer domain understanding.

Services

Domain Services are one of the foundational building blocks in Domain-Driven Design. While the term "service" is overloaded in software engineering, in DDD, a domain service has a very specific meaning and role. A Domain Service is a design pattern that encapsulates domain logic or functionality that doesn't naturally fit within an Entity or a Value Object. It operates on domain concepts and can be used to represent operations, actions, or queries.

Characteristics of Domain Services

  • Operates on the domain: Unlike application services or infrastructure services, a domain service should only contain business logic that pertains to the domain layer. It should be free of application-specific logic (like sending emails) or infrastructure logic (like database operations).
  • Stateless: Domain services don't hold state. They encapsulate domain logic, but they don't maintain any long-lived state of their own.
  • Acts as a bridge: In cases where an operation involves multiple aggregates or entities and doesn't clearly belong to any of them, a domain service can be the ideal place to put that logic.

When to Use Domain Services

  • When an operation spans multiple aggregates or entities: If an operation affects multiple aggregates and doesn't fit within any of them, a domain service is a suitable place to put that logic.
  • When an entity starts doing too much: If an entity becomes bloated or begins to take on responsibilities outside its primary role, consider moving that functionality to a domain service.
  • To decouple complex logic: For complex operations or logic that needs to be decoupled from entities for better maintainability and separation of concerns, a domain service is apt.

PHP Example

Suppose you have a rule that when a customer orders more than 10 products of a specific kind, they get a discount. This doesn't naturally fit within the Order or the Product. Instead, this logic can be placed in a domain service:

class DiscountService {

    public function applyBulkOrderDiscount(Order $order) {
        if ($this->qualifiesForBulkOrderDiscount($order)) {
            $order->applyDiscount(0.10);  // 10% discount
        }
    }

    private function qualifiesForBulkOrderDiscount(Order $order): bool {
        foreach ($order->getOrderLines() as $orderLine) {
            if ($orderLine->getQuantity() > 10) {
                return true;
            }
        }
        return false;
    }
}

Key Takeaways

  • Encapsulation: A domain service encapsulates domain logic that doesn't naturally fit in entities or value objects.
  • Clear Responsibilities: Having domain services can help ensure that the Single Responsibility Principle (SRP) is maintained in your domain layer.
  • Improves Model Expressiveness: Using domain services helps maintain a rich and expressive domain model.
  • Focus on the Domain: It's essential to ensure that domain services remain focused on domain logic and not drift into application or infrastructure concerns.

Collaboration with Other Building Blocks

Domain Services often collaborate with other DDD building blocks, including:

  • Entities & Value Objects: As we saw in the previous example, a domain service (DiscountService) worked in tandem with an entity (Order). Domain services frequently require entities and value objects to execute their operations.
  • Repositories: While domain services should be devoid of direct database or infrastructure operations, they might need to fetch or store domain objects via repositories. This enables them to make decisions based on the current state of domain objects.
  • Other Domain Services: Sometimes, a domain service might depend on another domain service to accomplish a task. It's important, however, to watch out for tangled dependencies to ensure the system remains maintainable.

Separation of Concerns

Domain Services should strictly concern themselves with domain logic. Avoid mingling application logic, user interface logic, or infrastructure concerns within them.

For example, while a domain service might determine that an email should be sent to a user, it shouldn't be responsible for the actual sending of the email. Instead, that responsibility should be delegated to an application service or an infrastructure service.

Granularity

Deciding on the granularity of domain services is more of an art than a strict science. It's based on the domain complexity, the team's preferences, and the evolving understanding of the domain.

  • Fine-grained Services: These perform very specific tasks. While they ensure a clean separation of concerns, they might lead to having many service classes, which could become hard to manage.
  • Coarse-grained Services: These handle broader sets of operations. While they might reduce the number of service classes, they could become large and less maintainable over time.

It's essential to strike a balance, ensuring each service has a clear responsibility without becoming a "catch-all" or too fragmented.

Testing Domain Services

Due to their stateless nature and focus on domain logic, domain services are prime candidates for unit testing. Since they encapsulate critical business rules, ensuring they're correctly implemented and thoroughly tested is vital. Mocking repositories or other dependencies allows you to isolate the service's logic in tests.

Domain Service vs Application Service

It's essential to differentiate between Domain Services and Application Services:

  • Domain Service: Contains business logic that's specific to the domain. It's part of the domain layer and is unaware of application-specific operations like user session management or external service integrations.
  • Application Service: Acts as a bridge between the domain and the application's needs. It orchestrates operations, may begin/end transactions, and can invoke domain services to perform business logic. When designing a system using DDD, always ask: "Is this logic core to the domain, or is it more about application orchestration?" This will guide where the functionality should reside.

Domain Service Discovery

Over time, as more domain services are added, it might become challenging to navigate and discover them, especially for new developers:

  • Documentation: Maintain updated documentation that describes each domain service's responsibilities. This not only helps in onboarding new team members but also serves as a reference during system enhancements.
  • Consistent Structure: Establish a consistent structure and naming convention for your domain services. This makes it easier to locate services related to specific domain operations.

Feedback Loop with Domain Experts

Maintain a continuous feedback loop with domain experts. As business requirements change, domain services will need to adapt. Regular discussions ensure that domain services remain aligned with the business's current understanding and needs.

Domain Services, when used effectively within the context of Domain-Driven Design, can greatly enhance the modularity, clarity, and maintainability of a system. However, they do not exist in isolation. Their design and evolution need to be seen in the broader context of the system, organization, and ever-evolving domain knowledge. Continuously revisiting, refining, and improving upon them ensures that they serve the business's needs effectively.

Factories

In DDD, Factories are responsible for encapsulating the process of creating complex domain objects or aggregates. They are especially valuable when:

  • Object Creation is Complex: Some domain objects have intricate creation logic, which isn't suitable to put into a constructor.
  • Object Creation Involves Multiple Steps: When creating an object requires multiple operations, a Factory can ensure that all steps are properly executed, and the created object is always in a valid state.
  • Different Creation Scenarios Exist: For instance, when you need to create objects from various sources (like from a database, from user input, or from an external system's response).

Advantages of Factories

  • Encapsulation: The details of object creation are encapsulated inside the Factory, separating the responsibility of creation from the client code that needs the object.
  • Single Responsibility: By offloading the creation logic to a Factory, the domain object can focus on its primary responsibilities.
  • Flexibility: You can replace or modify Factory methods without affecting client code.

Example in PHP

Suppose you have an Order domain object which has different ways to be instantiated. One way is to create a new order, and another is to recreate an order from stored data (e.g., from a database).

class Order {
    private $customerId;
    private $orderLines;
    // ... other properties ...

    private function __construct($customerId) {
        $this->customerId = $customerId;
        $this->orderLines = [];
    }

    public function addOrderLine($product, $quantity) {
        $this->orderLines[] = [$product, $quantity];
    }

    // ... other methods ...
}

Now, you can have a Factory for creating Order instances:

class OrderFactory {
    public function createNewOrder($customerId) {
        return new Order($customerId);
    }

    public function createOrderFromData($data) {
        $order = new Order($data['customerId']);
        foreach ($data['orderLines'] as $line) {
            $order->addOrderLine($line['product'], $line['quantity']);
        }
        return $order;
    }
}

Here, the OrderFactory provides two ways to create an Order:

  • createNewOrder: For brand-new orders.
  • createOrderFromData: When you need to reconstitute an order from saved data. By using the Factory, the client code doesn't need to know the intricacies of how an Order is created:
$orderFactory = new OrderFactory();

$newOrder = $orderFactory->createNewOrder($customerId);

$savedOrderData = /* fetch from database or other source */;
$existingOrder = $orderFactory->createOrderFromData($savedOrderData);

Factories play a critical role in maintaining the integrity and consistency of your domain model. By centralizing the creation logic, they promote cleaner, more maintainable code and ensure that domain objects are always created in a consistent, valid state.

Aggregates

An Aggregate is a cluster of domain objects that can be treated as a single unit. An aggregate will have one of its component objects be the aggregate root. Any references from outside the aggregate should only go to this root. This ensures the integrity of the aggregate's invariants and encapsulates its inner workings.

Purposes

  • Encapsulation: Aggregates help ensure that all the objects within it are in a consistent state. You can think of an aggregate as a protective barrier that doesn't allow external objects to hold references to its internal members. All external requests to modify the aggregate must go through the aggregate root.
  • Invariants Maintenance: Invariants are business rules that must remain consistent. Since aggregates form a boundary and encapsulation around certain domain operations, they are a primary mechanism for ensuring invariants are consistent after every operation on any of the contained entities. For instance, if there's a rule that says "an order can have a maximum of 10 items", it's the aggregate's responsibility to enforce this.
  • Simplifying Domain Logic: Aggregates help model the domain logic more concisely by grouping related entities and value objects.
  • Lifecycle Management: Aggregates can help manage the lifecycle of entities within the boundary. This is particularly useful when dealing with persistence mechanisms like databases.

Components

  • Aggregate Root: The primary entity inside the aggregate through which all external interactions happen. It's responsible for maintaining the aggregate's invariants. All external references should point only to the aggregate root, not to the internal entities.
  • Entities: Within an aggregate, there can be other entities, but they are not directly accessible from outside the aggregate.
  • Value Objects: These are immutable and don't have a global identity. They can also be part of the aggregate.

Practical Example

Consider an e-commerce system. A common aggregate might be the Order aggregate:

  • The Order entity would be the aggregate root.
  • Inside this aggregate, we might have OrderLine entities representing individual lines of the order.
  • Each OrderLine might have associated Product and Quantity value objects.

Here's a simplified structure:

class Order {  // Aggregate Root
    private $id;
    private $customer;
    private $orderLines = [];

    public function addOrderLine(Product $product, Quantity $quantity) {
        // This might enforce invariants like ensuring the number of order lines doesn't exceed a limit.
        $this->orderLines[] = new OrderLine($product, $quantity);
    }

    // ... other methods
}

class OrderLine {
    private $product;
    private $quantity;

    public function __construct(Product $product, Quantity $quantity) {
        $this->product = $product;
        $this->quantity = $quantity;
    }

    // ... other methods
}

In the above example, external objects or services can't directly modify the OrderLine objects. They must go through the Order aggregate root. This helps ensure that the Order aggregate remains in a valid state.

Important Considerations

  • Transaction Scope: Typically, you'll want to keep transactions aligned with aggregates. It means that a single transaction (e.g., a database transaction) shouldn't span multiple aggregates.
  • Consistency: Aggregates help maintain immediate consistency within their boundary. However, between aggregates, you may need to settle for eventual consistency, especially in distributed systems.
  • Design: When designing aggregates, it's essential to find the right balance. Making them too large can introduce unnecessary complexity, while making them too small might lead to challenges in maintaining consistency.

Aggregates are a powerful tool in DDD for modeling complex business domains, ensuring consistency, and simplifying domain logic. The key is to understand the business rules and design aggregates around them to ensure the invariants are always maintained.

Conclusion

Implementing DDD in PHP or any language requires careful thought, deep understanding of the domain, and close collaboration with domain experts. It's not just about coding but also about modeling the domain in a way that reflects its complexities and nuances. While DDD can introduce more complexity in terms of design and structure, it can lead to more maintainable and scalable software in domains with complex business logic.