When faced with refactoring a poorly architected project, or designing a new one, choosing the right patterns can be overwhelming. The Tactical Decision Tree, popularized by Vlad Khononov in his book "Learn Domain Driven Design", offers a clear, step-by-step guide. It helps you select the appropriate tactical patterns from Domain-Driven Design (DDD) based on the specific needs of different parts of your application. This ensures that you invest complexity where it's needed and keep things simple everywhere else.
The tree begins with the most fundamental question, which serves to partition your system logically.
Determine the Subdomain Type
The first and most critical step is to identify the type of subdomain you are working on. In DDD, the entire business domain is divided into several subdomains.
Core Subdomain: This is the part of your business that delivers the primary value and constitutes your competitive advantage. The most effort and the most sophisticated patterns are applied here. For example, in a ride-hailing service, the core subdomain would be the algorithm for matching drivers and calculating fare prices.
Supporting Subdomain: These parts of the system are not the core advantage but are necessary for the main functionality to work. They can be complex in their own right but can be implemented using simpler approaches. For the same ride-hailing service, a module for generating accounting reports would be a supporting subdomain.
Generic Subdomain: These are solved problems for which ready-made solutions or libraries exist. Examples include user authentication systems or services for sending email notifications.
Your first task in refactoring is to classify each piece of your project into one of these three categories.
Path 1: The Supporting or Generic Subdomain
If you determine that a piece of logic belongs to a supporting or generic subdomain, you follow the left branch of the tree. The goal here is to implement the functionality reliably but with minimal effort, allowing you to focus your resources on the Core Subdomain.
The question:
Are the data structures complex?
No:
If the logic is simple (e.g., standard Create, Read, Update, Delete operations), the best choice is the Transaction Script pattern.
A Transaction Script is a single procedure that takes a request, executes one business transaction, and returns a response. All the logic is contained in one place. This is ideal for straightforward operations like "create a user" or "update profile settings."
Yes:
If the logic is still not core but involves interconnected data (e.g., an order and its line items), the Active Record pattern is a suitable choice.
An Active Record is an object that acts as a wrapper around a row in a database table. It contains both the data and the methods for saving, updating, and deleting it. This pattern is well-known from frameworks like Ruby on Rails and Laravel.
For both the Transaction Script and Active Record patterns, the recommended approach is the classic Layered Architecture (3 or 4 layers). This time-tested architecture (Presentation Layer, Application Layer, Domain Layer, Infrastructure Layer) provides a good separation of concerns for the less complex parts of a system. Transaction Script pattern combines the Application and the Domain layer in (usually) one procedure — thus only 3 layers for it.
Path 2: The Core Subdomain
If you are working on the core of your business, you proceed down the right, more sophisticated branch of the tree. This is where you should not cut corners on design.
The question:
Are monetary transactions, an audit log, or analysis required? This question is a proxy for a more fundamental one: Do we need to store the complete history of all state changes?
Yes:
If the answer is yes, as is the case with financial operations, audit logging, or analytics where history is crucial, the best choice is an Event Sourced Domain Model.
Instead of storing the current state of an object (like an account balance), you store the sequence of events that happened to it ("Account Opened," "Deposited $100," "Withdrew $20"). The current state is calculated by "replaying" all these events. This provides a 100% auditable trail and allows you to analyze the system's state at any point in the past.
For Event Sourcing, the CQRS (Command Query Responsibility Segregation) pattern is almost always a preference. CQRS separates the models for writing data (Commands) from the models for reading it (Queries). This is because reading data directly from a log of events is often inefficient and impractical for user-facing applications.
No:
If you don't need a full history and only care about the current state of your objects, you should choose the classic Domain Model (often called a "Rich Domain Model").
This is an object model where data and complex business logic are encapsulated together within objects (Entities, Value Objects), while transactional behavior is managed by an abstraction of an Aggregate, bounded by the root entity. This is the heart of classical DDD, where you model business rules with the highest possible fidelity.
If the same data model is suitable for both changing and displaying data, then the Ports & Adapters (Hexagonal Architecture) is the recommended architecture.
This architecture isolates your core business logic (Domain Model) from the outside world (databases, UI, external APIs). The logic communicates with the outside through "ports" (interfaces), while specific technologies (like PostgreSQL or a REST API) are implemented as "adapters." This makes the system highly flexible and easy to test.
If you need different data structures for write operations versus read operations – like an optimized structure for reading data (e.g., for complex reports or fast UI rendering), the CQRS pattern is once again the answer. You would use your rich Domain Model to execute commands (data changes) while creating separate, often "flat," models specifically for handling queries (data reads).
How to Apply This to Your Refactoring Effort
Analyze: Go through your existing project and classify its features into Core, Supporting, and Generic subdomains.
Follow the Tree: For each subdomain, honestly answer the questions in the decision tree about data complexity and audit requirements.
-
Choose Your Pattern: The tree will guide you to a starting point:
- For a simple admin panel with CRUD forms, use a Transaction Script with a Layered Architecture.
- For managing a product catalog, Active Record with a Layered Architecture might be sufficient.
- For your core pricing and ordering logic, use a Domain Model with Ports & Adapters.
- For a financial module or any system where auditing is critical, use Event Sourcing with CQRS.
This decision tree is not an inflexible rulebook but a powerful mental model. It helps structure your thinking and enables you to make conscious, deliberate architectural decisions rather than applying a one-size-fits-all solution to your entire system.
For more about managing architectural decisions, take a look at my article about Architecture Decision Records.
Top comments (0)