Tactical Domain-Driven Design

In my earlier article on Domain-Driven Design, I discussed how DDD is typically divided into two primary categories: Strategic Design and Tactical Design. While Strategic Design focuses on understanding the "Why" behind DDD — the overarching principles and reasoning — Tactical Design addresses the "How." 

Tactical DDD offers practical methods and tools to implement DDD effectively by introducing the following key concepts and it provides a set of techniques that help bring clarity and structure to complex systems. These tools are particularly useful for creating well-defined, maintainable, and scalable code. These include:

  1. Business Logic Implementation Patterns
  2. Bounded Context’s Architectures
  3. Bounded Context’s Communication Styles

Additionally, by using these tactical patterns, development teams can better communicate and collaborate with domain experts, ensuring that the software reflects real-world business logic. This alignment between technical and business perspectives is what makes DDD, particularly the tactical aspect, a powerful approach to building meaningful and robust software systems.

Business Logic Implementation Patterns

In software development, a system often encompasses multiple subdomains, each with varying levels of technical complexity and impact on the business and its objectives. Given these differences, it becomes essential to adopt tailored patterns for implementing business logic. These patterns allow developers to deliver the required features effectively, ensuring that the solution aligns with the unique needs of each subdomain while supporting overall business goals.

By recognizing the significance and distinct value of each subdomain, teams can choose the most suitable approach for managing complexity. Whether a subdomain is highly critical to business success or plays a more supportive role, the implementation pattern should reflect its importance.

Transaction Script Pattern

The Transaction Script pattern organises the business logic within a bounded context into distinct procedures or transactional steps. Each step represents a specific business operation, allowing the logic to be broken down into smaller, manageable units. A common example of this pattern is the ETL (Extract, Transform, Load) process, where data is extracted, cleaned, and transformed before being loaded into its target destination.

 

Transaction Script Pattern Transaction Script Pattern

Due to its straightforward nature, the Transaction Script pattern is often the preferred approach when dealing with supporting subdomains, where business logic tends to be simpler and does not require the complexity of more advanced architectural patterns. Its simplicity and ease of implementation make it an ideal solution for these scenarios, where quick and efficient development is essential, without compromising on maintainability or clarity.

Active Record Pattern

The Active Record pattern is well-suited for scenarios where business logic is straightforward, similar to the transaction script pattern. However, what sets Active Record apart is its ability to handle more complex data structures, which cannot be effectively managed with a simple transaction-based approach. Active Record excels when dealing with operations that require direct interaction with a database, particularly when performing CRUD (Create, Read, Update, Delete) operations.

A key advantage of the Active Record pattern is its integration of data access and business logic within the same object. This design simplifies development in systems with uncomplicated domain rules, as it reduces the overhead of managing separate layers for persistence and logic. However, as the complexity of business rules grows, this pattern may become less suitable, as it can lead to tightly coupled code that is harder to maintain or scale. In such cases, more sophisticated approaches, such as the Domain Model pattern, may be more appropriate.

Domain Model Pattern

Business challenges are often too complex to be effectively represented by simpler patterns like active records or transaction scripts. To address the intricacies of real-world business domains and offer appropriate solutions, more advanced and robust patterns are required.

Consider the scenario where a client requests the development of an online retail platform. To meet various requirements, including service level agreements (SLA), scalability, extensibility, and security, relying solely on the aforementioned patterns is insufficient. A Domain Model offers a more sophisticated approach, capable of representing intricate business problems. This pattern is built upon two interconnected and complementary descriptions.

The Domain Model in Domain-Driven Design allows for a deeper understanding of the business logic by aligning the model with the core domain of the business. It encapsulates complex rules, relationships, and behaviors, making it easier to scale, adapt, and enhance over time, ensuring that the software reflects the evolving nature of the business.

A domain model is an object model of the domain that incorporates both behavior  and data.
-- Martin Fowler, Patterns of Enterprise Application Architecture

Domain Model, the pattern to model complex business problems, is the combination of two separate and complementary descriptions:

Domain: It is the business related word and represents the problem that we want to solve

Model: It is the process map that shows the solution for the business problem inquired by domain

Domain model design pattern made up of components or building blocks that we are going to learn them in this in the following.

Value Object: It is an entity in Domain-Driven Design that is identified by its attributes rather than by a unique identifier. Its identity is entirely defined by its values. For instance, a color object can be described by its RGB components, or an object representing a house can be characterized by attributes such as street name and zip code. These values are inseparable from the object’s identity, meaning that two objects with the same values are considered equal.

From a developer's point of view, Value Objects are useful because they encourage immutability, making the code easier to reason about and less prone to errors caused by unintended side effects. Since they are defined by their values rather than an identity, developers can easily compare, copy, and replace them.

From an architectural point of view, Value Objects provide clear boundaries within the domain model. They encapsulate and organize key concepts in a way that ensures the system remains focused on the problem domain, rather than implementation details. This aligns with DDD principles, emphasizing a deeper understanding of the domain while maintaining simplicity.


Entity: In Domain-Driven Design unlike value objects, an entity is an object with a distinct identity that persists over time, even as its state changes. A common example is a person who has attributes such as name, age, and ID. While characteristics like the name or age may vary, the person's unique identifier (ID) remains constant, ensuring continuity in identity despite changes in other attributes.

From a developer's point of view, entities are crucial because they embody the core concept of identity within a system. Developers work with entities to ensure that even as data evolves, the continuity of the object’s identity is maintained.

From an architectural point of view, entities form the backbone of the business model. Architects design the system’s structure to ensure that entities are handled in a way that maintains their integrity across various processes. This involves defining how entities interact with other parts of the system, ensuring consistency, scalability, and proper handling of complex state changes over time.


Aggregate: In Domain-Driven Design, it is a collection of related objects, which can include both value objects and entities, that are treated as a single unit. This grouping ensures that the business rules, or invariants, are consistently maintained across the entire set. The aggregate functions as a state machine, meaning it governs the state changes of its internal objects to ensure integrity. 
Aggregate
Access to the aggregate is controlled through a designated entity known as the aggregate root, which serves as the only entry point to interact with the aggregate. The rest of the objects inside the aggregate can only be modified or accessed indirectly through this root entity, ensuring encapsulation and protecting the business logic.


From a developer’s standpoint, aggregates simplify the handling of complex business logic by grouping related objects together. They provide clear boundaries that ensure business rules are consistently applied, making the codebase easier to maintain and extend.

For an architect, aggregates are a key design principle that helps in structuring a domain model efficiently. They define clear boundaries within the system and ensure that each aggregate is responsible for its own consistency.  The aggregate pattern also aligns well with the principles of microservices architecture, where aggregates often map to individual services.


Domain Event: It is a message that captures and conveys an important occurrence within the business domain. It represents a meaningful event that has taken place in the system, typically associated with an aggregate or entity in the domain model.
Domain Event
 When something significant happens in the domain—such as a state change or completion of a business process—the aggregate root (the main entry point of the aggregate) will publish the domain event. A typical example is an event triggered when a change is detected in a relational database, such as through CDC (Change Data Capture) tools like Debezium.

From developer's perspective, domain events provide a way to decouple different parts of the system, making it easier to manage complex workflows and implement event-driven architectures

From architectural point of view, domain events are crucial for building distributed systems. They enable asynchronous communication between different bounded contexts or microservices, allowing the architecture to scale and adapt over time. Domain events also serve as a key mechanism for maintaining consistency in eventual consistency models and ensuring that changes across systems are propagated efficiently.


Bounded Context Architecture
Bounded context architecture in Domain-Driven Design encompasses various methods and strategies for facilitating communication between components within a bounded context. These strategies, referred to as architectural patterns, provide solutions for implementing both functional and non-functional requirements in the codebase.

Choosing the right architectural pattern is vital, as it directly influences both the business domain and the overarching architectural decisions in the short and long term. A thoughtful selection can enhance collaboration between teams, improve system scalability, and ensure that the software evolves in alignment with business goals, ultimately leading to more effective and sustainable solutions.

In this section we discuss 3 main application architectural patterns as here:

Layered Architecture

Layered architecture in Domain-Driven Design is structured into three distinct and independent layers, reflecting the concept of bounded contexts within the code-base. In this architecture, each layer can only communicate directly with the layer immediately above and below it. This means that the top layer, which handles presentation, cannot interact directly with the bottom layer responsible for data access; all interactions must go through the business logic layer. This separation ensures a clear distinction of responsibilities and promotes better maintainability and scalability of the system.

Moreover, this architectural approach encourages a clean separation of concerns, allowing teams to work on different layers independently. By isolating the presentation, business logic, and data access, developers can implement changes in one area without affecting others, thereby enhancing the overall robustness and adaptability of the application.
Layered Architecture

 

Presentation Layer: This layer manages how customers interact with the system and implements the user interface. In console applications, for instance, the command line interface (CLI) facilitates user interaction, while in web applications, the graphical user interface (GUI) serves this purpose. Additionally, APIs or gateways can also function as presentation layers, enabling communication between users and the system.

This layer is important because it shapes the overall user experience and ensures that interactions are intuitive and effective. By presenting data and features in a user-friendly manner, it enhances accessibility and engagement, making it easier for users to achieve their goals.

Business Logic Layer: In a layered architecture commonly employed in Domain-Driven Design (DDD), the Business Logic Layer serves as the core component where business logic and requirements are meticulously implemented. This layer is crucial for ensuring that business rules are consistently applied throughout the application. Patterns such as Active Record and Transaction Script, previously discussed, are integral to this layer, facilitating effective data management and business process execution.

Additionally, the Business Logic Layer acts as a bridge between the Presentation Layer and the underlying data sources, ensuring a clear separation of concerns. By encapsulating business rules and operations, it allows for more flexible and maintainable code, enabling developers to adapt to evolving business needs without compromising the integrity of the presentation or data layers.

Data Access Layer: In traditional layered architecture, the Data Access Layer is primarily tasked with managing database interactions. However, in contemporary implementations, this layer has evolved to address data persistence across diverse data types and storage solutions, including both SQL and NoSQL databases. This adaptation allows for more flexible data management strategies that accommodate the complexities of modern applications.

Additionally, the Data Access Layer ensures that data is retrieved and stored in a manner that aligns with the overarching principles of Domain-Driven Design. By abstracting data access logic, it not only enhances maintainability but also promotes a clear separation of concerns, enabling developers to focus on business logic within the application.

Port & Adapter (Hexagonal) Architecture

Hexagonal Architecture represents an evolution of the traditional layered architecture pattern, providing a more robust structure for developing applications. The choice of a hexagon as the foundational shape for this architectural style is not based on any specific rationale but rather on its ability to encapsulate various components effectively. As depicted in the accompanying illustration, Hexagonal Architecture comprises three layers:

  1. Infrastructure
  2. Application
  3. Business Logic

Notably, the business logic layer, which serves as the high-level module, operates independently from the infrastructure layer, the low-level module. This independence underscores a fundamental principle of the architecture: the dependency inversion principle.

Port & Adapter (Hexagonal) Architecture

In addition to enhancing modularity, Hexagonal Architecture promotes better testability and adaptability. By decoupling the core business logic from external influences, such as user interfaces and data sources, developers can easily swap out implementations without affecting the overall functionality. This flexibility not only streamlines the development process but also facilitates a more agile response to changing business requirements.

Business Logic Layer: It serves as the core of our domain models, encapsulated by the Application Layer. In this framework, the Application Layer functions as a mediator, facilitating interactions with the domain models found in the Business Logic Layer.

In the context of layered architecture and Domain-Driven Design, this separation of concerns enhances the maintainability and scalability of the application. By clearly delineating the roles of the Application and Business Logic Layers, developers can more easily manage complexity, allowing for robust domain modeling that accurately reflects the business rules and processes. This approach not only fosters better collaboration between technical and business stakeholders but also promotes a more adaptable architecture, enabling the system to evolve in response to changing requirements.

Infrastructure Layer: This layer in Hexagonal Architecture serves as the external interface between the core application and the outside world. It is responsible for handling all interactions with external systems such as databases, message brokers, file systems, APIs, and user interfaces.

A key advantage of the Infrastructure Layer is that it enables easy replacement or modification of external systems without altering the core business logic. Moreover, the Infrastructure Layer plays an important role in enabling flexibility and scalability. It can accommodate changes in technology or third-party services without impacting the core business logic.

Application Layer: The Application Layer serves as the orchestrator of the business logic in both Hexagonal Architecture and Layered Architecture as applied in Domain-Driven Design. It is responsible for coordinating and implementing the core use-cases of the system, without being concerned with the finer details of infrastructure or the persistence layer.

In complement to its primary function, the Application Layer also plays a key role in maintaining a clean separation of concerns. It abstracts away the intricacies of interaction between the domain and external interfaces. By isolating the core business logic from the technical details, the architecture becomes more resilient to evolving requirements.

Ports: In the context of Hexagonal Architecture within DDD, ports play a pivotal role in facilitating communication between the business logic layer and the application layer. Ports serve as interfaces, allowing access to use cases while preventing direct interaction between the business logic and infrastructure components. By establishing a clear boundary, they enable a decoupled and flexible system architecture, supporting both ingress and egress communication pathways in a seamless manner.

Ports are essential for maintaining the integrity of the domain by abstracting dependencies and promoting adherence to core business logic. Acting as a gateway and decoupling external systems from infrastructure, enhances test-ability and makes the system more adaptable to future enhancements and integrations.

Adapter: An Adapter refers to the implementation of a corresponding port within the infrastructure layer, designed to manage interactions with various technology stacks, such as databases, cloud services, or external APIs. In essence, adapters serve as flexible components or plugins that translate the domain’s input and output into a format that external systems can understand, and vice versa. Furthermore, adapters contribute to the decoupling of the application’s core logic from the underlying infrastructure, which facilitates easier testing, modularity, and future adaptability.

Command-Query Responsibility Segregation Architecture

In real-world projects, as in our business, there is no such thing as a perfect, singular business domain model that satisfies all the requirements of a domain. A single database or data type alone is insufficient to fully represent the system’s demands. To address this complexity, a polyglot persistence model, which supports the use of multiple types of databases, becomes essential in handling diverse data needs efficiently.

To tackle the limitations inherent in defining a one-size-fits-all model for a business domain, Greg Young introduced the CQRS pattern. This approach helps overcome these challenges by dividing the domain into two distinct models:  command model and read model.

Command-Query Responsibility Segregation Architecture

Moreover, this pattern allows teams to optimize the performance of each model independently. While the command model can focus on business logic and data consistency, the read model can be fine-tuned for performance and scalability, providing a more efficient and adaptable solution to meet varying domain requirements.

Command Model: The Command Model represents the central mechanism responsible for modifying the system's state in the context of Command Query Responsibility Segregation as applied in Domain-Driven Design. It serves as the authoritative source for executing operations that change the state of the application, ensuring that all business logic, validations, and state transitions occur consistently. Through the Command Model, operations such as rule validation, often governed by a policy engine, are executed to enforce domain-specific constraints and rules before any change is applied.

Read Model: In the context of CQRS, a Read Model serves the specific purpose of delivering data and information tailored to the unique needs of different systems and users. Unlike a write model, which focuses on processing commands and mutating the system's state, the read model is designed to provide optimized views of data, often denormalized, to support efficient querying and retrieval.

As a complementary feature, the read model enhances scalability and performance by reducing the burden on the core domain model, allowing queries to be handled without disrupting ongoing transactions or command processes.

Separating the command and query responsibilities into two distinct models advocates for maintaining independent models for handling commands (which modify data) and queries (which retrieve data). Unlike the command model, which is the sole model responsible for making changes in the system, multiple read models can exist, each tailored to specific query needs.

 

 

Furthermore, the process of updating the read model can be accomplished in two distinct methodologies: synchronously or asynchronously. The choice between these methods, detailed further in the next section, depends on the requirements of the system, including factors such as consistency and performance.

Bounded Context's Communication Styles

In Domain-Driven Design, facilitating effective communication between bounded contexts presents a significant challenge, primarily because of its profound impact on non-functional requirements such as system performance, scalability, and maintainability. The complexity lies in ensuring that information flows seamlessly while preserving the integrity of each bounded context.

Despite its challenges, the communication layer is also one of the most transparent and well-defined areas of DDD, regarding to the clear communication styles that guide how contexts interact with one another. These communication styles, whether synchronous or asynchronous, dictate the behavior and reliability of distributed systems. Choosing the right style requires careful consideration of factors such as latency, consistency, and the nature of the dependencies between contexts.

Synchronous Communication: In Domain-Driven Design, when a bounded context engages in synchronous communication with another, it signifies that the initiating context pauses its execution, awaiting a response before proceeding with its operations. This approach ensures that no further actions are taken until the called bounded context has provided the necessary feedback. One of the most widely adopted patterns for synchronous communication is REST, where two endpoints interact directly in real time, relying on the immediate availability of the service and data.

For architects and developers, the foremost non-functional requirement driving the choice of synchronous communication is the assurance of data consistency and service availability, as both are crucial for maintaining system reliability.
Furthermore, synchronous communication fosters a tighter coupling between systems, ensuring that requests are processed in sequence and in real-time. However, this style requires careful consideration of the system's resilience and scalability, as any failure or delay in one context can propagate across the system, impacting overall performance.

Asynchronous Communication: Waiting for feedback from a called service is often unnecessary and can lead to various challenges, such as performance bottlenecks within the bounded context. When the immediate response or feedback is not critical to the caller’s operations, adopting an asynchronous communication style becomes the optimal solution. By decoupling the communication process, with help of asynchronous communication style, the system gains increased flexibility and efficiency, allowing each bounded context to function independently without being hindered by delays or potential failures in other services.

Asynchronous messaging, facilitated by brokers like Kafka, RabbitMQ, and AWS SQS, is particularly effective in managing complex interactions across distributed systems. These tools enable seamless communication by allowing messages to be queued and processed independently, thus improving scalability and resilience.

Wrapping Up

Tactical Domain-Driven Design provides a structured approach to implementing business logic in software applications. It emphasizes the importance of aligning the software design closely with the business domain, thereby ensuring that the application accurately reflects the complexities and nuances of the business processes it is meant to support. By leveraging various implementation patterns, such as entities, value objects, aggregates, repositories, and domain services, developers can create a cohesive and maintainable codebase that encapsulates the essential business rules and behaviors.

Bounded contexts are a core concept in DDD, acting as a boundary within which a particular domain model is defined and applicable. They help to manage complexity by dividing the system into distinct areas, each with its own model, ensuring that different parts of the application do not interfere with each other. By clearly defining the boundaries of each context, teams can work independently and avoid the pitfalls of shared models that can lead to ambiguity and inconsistency.

Communication between bounded contexts is another critical aspect of tactical DDD. Different contexts may need to interact, and the way they communicate can significantly impact the system's overall effectiveness.
Last but not least, Tactical Domain-Driven Design provides a comprehensive framework for developing complex software systems by focusing on the business domain, establishing clear boundaries through bounded contexts, and facilitating effective communication.

Share This Article