Domain-Driven Design: Approaches and Patterns for Navigating Complexity in Software Development.
Blog Detail Page
Blog Detail Page
Blog Detail Page

Domain-Driven Design: Approaches and Patterns for Navigating Complexity in Software Development.

Blog Detail Page
Blog Detail Page
Blog Detail Page

This article delves into the core principles of Domain-Driven Design (DDD) and its essential strategic patterns—like ubiquitous language and bounded contexts—to align applications with business requirements, enhance team communication, and tackle complexity in software systems.

In the past, application development was often seen as distinct from business operations, involving an initial phase of requirements collection to define needs, followed by the delivery of a final product that frequently fell short of expectations. This approach, known as the waterfall method, offered little room for adjustments during the process.

By 2001, the Agile Manifesto shifted the focus toward embracing change in development practices. However, a key challenge remained: bridging the gap between the business (including its entities, their interconnections, and the operations performed on them) and how these were represented in the resulting application.

In late 2003, Eric Evans’ book, Domain-Driven Design: Tackling Complexity in the Heart of Software, introduced a fresh perspective. Dubbed the ‘DDD blue book,’ it wasn’t a hands-on guide to applying patterns but rather a framework for ensuring the application accurately reflected the business, providing strategies to achieve this.

DDD (Domain-Driven Design) is especially valuable when developing applications in intricate knowledge domains. It’s not always the ideal choice—given that relatively simpler domains may benefit from less complex methods that balance construction cost and effort. However, in cases of significant domain complexity, this approach offers greater stability of the application to adapt to the inevitable changes that arise as the business evolves.

Below, we outline some guidelines for implementing DDD.

Employing a ubiquitous language

Frequently, we encounter applications with tables, data models, and names that, after a few years, become unclear to everyone involved, hindering comprehension among all parties.

This is exactly what a ubiquitous language seeks to prevent.

A ubiquitous language is a shared vocabulary used across all teams in a project, designed to reduce misunderstandings and promote collaboration between developers and domain experts.

A ubiquitous language should:

  • Be domain-focused: meaning it reflects how domain experts—those with deep knowledge of the business area or problem the software addresses—describe their work. It’s impractical for developers to use different terms that might cause confusion.
  • Be uniform: this requires consistently using the same terms for the same ideas. If multiple terms exist for a concept, one must be selected and agreed upon for consistent use.
  • Be clear and specific: ambiguity must be avoided, and the same term should not refer to different concepts.
  • Bridge software and business: by incorporating ubiquitous language terms into the code (such as class names, methods, or modules), it becomes easier for all involved to follow.

This method ensures a clear way to articulate domain challenges and solutions that all teams can grasp. The same applies to the code. Developers can communicate with the business using a shared language.

Let’s consider a practical example:

  • Domain experts inform us that Bank Accounts exist, each with an Account Holder, a Balance, and a ValueDate tied to that balance. An amount can be Deposited, increasing the Balance, or Withdrawn, decreasing the Balance provided the amount doesn’t exceed what’s available. In both cases, the ValueDate is refreshed
  • The code might look like this:
    • BankAccount Class
      • Holder
      • Balance
      • ValueDate
    • With methods such as:
      • Deposit(amount, date)
      • Withdraw(amount, date)
    • A pseudocode example could be:
      • Deposit(amount, date): Balance = Balance + amount, ValueDate = date
      • Withdraw(amount, date): If Balance < amount ThrowError Else Balance = Balance − amount, ValueDate = date

Using this example as a foundation, domain experts might now ask for a monthly report of accounts inactive over the past month. The development team could then suggest filtering BankAccounts where the ValueDate precedes the current month.

This ensures that what’s requested and what’s built are fully aligned, with everyone clear on what will be done and how it functions.

In general, ubiquitous language

  • Enhances communication within the team and avoids misunderstandings
  • Aligns business and technology, as it ensures that what is implemented meets real needs
  • Leads to more readable, accurate and maintainable code, avoiding confusion during development
  • Improves collaboration between areas by making technical discussions more accessible to business

Bounded context

By segmenting our application into distinct contexts, we can break down a complex system into more independent and manageable components.

The concept is that each context maintains its own ubiquitous language, and the business is modelled to address a specific segment of the broader problem. Within this limited scope, we ensure the domain carries a clear meaning, remaining logical and aligned with the business rules specific to that segment.

These separate contexts are known as bounded contexts.

A key advantage of bounded contexts is their well-defined limits, which isolate them from the rest of the application, ensuring that modifications within one don’t impact others.

There are also patterns that support interaction between bounded contexts while preserving their separation (such as APIs, events, or anti-corruption layers).

An example: in a banking organization, there might be a bank accounts context and a loans context, each able to evolve independently despite potential connections.

Furthermore, the bounded context concept can be leveraged as needed to simplify complexity by creating smaller, cohesive sub-contexts within a larger context, where functionality can be developed separately from the rest.

Practically, to pinpoint a bounded context in our domain, we look for areas with a distinct set of terms (the ubiquitous language for that context) or unique rules, areas with functional needs that differ significantly from others, or areas that naturally separate workflows or business processes.

Bounded contexts are crucial for avoiding system becoming monolithic, as they clearly delineate the various models within the business and enable technical options like distributed architectures.

Context map

As noted earlier, when we divide our system into multiple bounded contexts, we may end up with numerous smaller contexts, and it’s essential to understand how they will interact.

This is where the context map comes in—a visual depiction of the bounded contexts and their interactions, whether hierarchical or collaborative.

In a hierarchical setup, the leading context dictates its terms, and the subordinate contexts typically adjust accordingly.

Common integration patterns in this scenario include the Conformist pattern, the Anti-corruption layer pattern, the Shared kernel pattern, or the Customer-supplier pattern.

At times, the defined contexts work together as equals, requiring different patterns for collaboration. These include the Partnership pattern, the Shared kernel pattern (which applies to both hierarchical and collaborative interactions), the Published language pattern, and the Separate ways pattern.

Deciding which pattern to apply is a key aspect of the craft of domain-driven design, but here are some guidelines:

  • If one context relies fully on another to operate effectively, a hierarchical pattern is more suitable. For instance, the pricing context might be subordinate to the product context.
  • If both contexts are independent and can function without one overshadowing the other, a collaborative pattern is preferable. Take the order context and shipping context, for example—they’re related but can collaborate autonomously.
  • If one context consistently performs actions dictated by another, it’s a hierarchical relationship. However, if the domains only need to exchange information, it’s a collaborative relationship.
  • In a hierarchical pattern, if the subordinate domain can adopt the dominant domain’s model, the Conformist pattern is ideal. But if the subordinate domain needs to preserve its own model and rules for internal consistency, patterns like Anti-corruption layer or Shared kernel are better choices.
  • If one domain shapes the design of another, collaboration patterns like Partnership are more appropriate.

For mapping contexts, rectangles or boxes typically represent the contexts, with arrows showing their relationships. These arrows can be unidirectional for hierarchical ties and bidirectional for collaborative ones. These arrows also specify the chosen pattern (e.g., Conformist, Partnership). Often, if known at the mapping stage, the communication method (API, event, code integration, etc.) is noted as well.

Below, we’ll briefly outline the primary patterns for reference.

> Confirmist pattern

This hierarchical pattern involves the subordinate context fully adopting the model and rules of the dominant context. It does make any effort to alter or adjust it to preserve its own model. Its benefit lies in straightforward integration, though it limits the context’s independence and leaves it vulnerable to shifts in the dominant context.

> Anti-corruption layer pattern

This hierarchical pattern focuses on safeguarding one’s own model against the dominant context’s model through a translation layer that serves as an intermediary. It offers the advantage of shielding the context from changes in the dominant context while maintaining an independent model, but it demands more implementation work and may lead to performance issues due to the added translation layer.

> Shared kernel pattern

This pattern can function as either hierarchical or collaborative. It relies on a shared portion of the model between both contexts, while allowing independent evolution in other areas. Its strength is in minimizing duplication of common concepts and ensuring consistency of shared data, though changes to the shared kernel affect both domains and necessitate strong teamwork between the groups managing them.

> Customer-supplier pattern

This hierarchical pattern positions the subordinate context as a customer and the dominant context as a supplier. Here, the subordinate can shape the dominant’s decisions by consuming data or features tailored to its needs. Its advantage is that it promotes collaboration and aligns well with the subordinate’s requirements, but it demands ongoing, active coordination, and if the dominant serves too many subordinates, it can create extra workload.

> Partnership pattern

This collaborative pattern is ideal when both contexts function as equal partners, collaborating closely toward a shared objective. Ongoing coordination between the two ensures they remain compatible. Its strength lies in enabling a balanced, coordinated evolution of both contexts with necessary integration, though, as noted, it requires constant coordination, and differing work paces between teams can lead to issues.

> Published language pattern

This collaborative pattern involves creating a protocol or method for interaction, allowing a context to share its model clearly and consistently with others. It supports connections with multiple contexts, not just one. A technical example is publishing APIs with well-defined messaging via OpenAPI. Its benefits include clear integration and the ability to engage with various contexts, but it may demand adjustments from consumers when the published language updates, and it must be kept current and well-documented.

> Separate ways pattern

This collaborative pattern fits when two contexts choose not to interact and can address their challenges independently. While it often leads to duplicated data or business logic, it fosters autonomy and independence, minimizing the chance of one context impacting the other.

Conclusions

Domain-Driven Design provides an approach to managing the complexity of an application system.

Well-defined contexts enable the domain to be split into controllable segments, while strategic patterns support clear and identifiable integration methods.

Embracing these techniques enhances team autonomy and application adaptability.

Most importantly, it fosters effective communication between teams and ensures the domain model evolves alongside business requirements.


Sigue leyendo…