Getty Images

Tip

Using bounded context for effective domain-driven design

Domain-driven design helps organizations develop software focused on key business needs. But to do so, architects need to understand the fundamentals of bounded context.

Architects often struggle to implement pure domain-driven design across an organization. This is an effort that requires mapping complex business processes across multiple departments and software systems, and the complexity of this task makes it hard to achieve the desired outcome.

However, domain modeling is a good approach to translating business demands into a technical design, and it behooves architects to understand how to build the bounded contexts that define effective domain-driven design.

Understanding bounded context

In his book Domain-Driven Design: Tackling Complexity in the Heart of Software, software design consultant and author Eric Evans provides a useful definition of bounded context. He starts by describing how a proper bounded context helps map large business domains to the individual underlying software systems that support them. In this sense, a bounded context is a pattern that guides the rules and boundaries needed to keep large software systems in check.

A bounded context ensures that the defined domain model makes sense. It should be clear to all those who touch that system where the edges of bounded context are. If this is not made sufficiently clear, it can result in a domain model that is ineffective and convoluted.

One of the most effective ways to convey the specifics of a bounded context is to establish identifying names and use them consistently. For instance, clearly articulating the definition of a user, customer, administrator or product can help teams clearly understand what they mean and their relationship to the system. This also ensures that different words don't describe the same concepts, which can quickly confuse teams responsible for large-scale systems.

Strategies for mapping bounded context

There are a few different ways that teams can map bounded contexts across systems.

Shared kernel

A shared kernel is a pattern where two or more teams share the code that underlies two distinct bounded contexts. This means that teams are mutually dependent on common domain objects. Teams must discuss any planned changes to the domain model to ensure that the model's intent doesn't change over time.

Conformist

When a single team takes primary control of the domain model, downstream consumers must evaluate if they can conform to the domain model of the upstream system as is. This attitude of "take it or leave it" by downstream consumers is called the conformist approach.

Anti-corruption layer

In cases where downstream consumers cannot accept the upstream domain model as is, it's possible to add a translation layer that converts the upstream context into its own bounded context. This transformation layer is often termed an anti-corruption layer (ACL) and takes primary responsibility for mapping bounded context.

Open host service

When a domain model experiences a stronger influence by downstream consumers than the upstream service, it's possible to follow a "reverse" ACL process. Instead of the downstream service writing an ACL, the upstream service can expose a published specification that, while written differently than the internal context, maps the internal domain model to the published specifications. This allows the implementation to evolve without directly affecting downstream consumers.

Defining object types in the domain model

We have seen how teams can apply different patterns to map bounded contexts across a large software system. However, within each bounded context, defining the context and the ubiquitous language that defines the bounded context and the domain model is imperative. Once a team maps different object types within a domain, it's possible to construct a domain model that maps the business model within the guidelines of the bounded context.

Entity

Sometimes, objects that share common attributes must still maintain their own unique identity. In cases like this, teams must identify objects within a domain by something other than a description of their basic attributes. Objects that need their own defining identity are termed "entities" and comprise the domain model's core building blocks.

For instance, imagine a software system allowing users to pick their user names -- including one already used by another user. These users will still need their distinctive identities in the system to avoid confusion.

Value objects

Entities are an element of domain-driven design that have a unique identity of their own. There is another element type called value objects. Value objects are essential to domain-driven design as they represent immutable, unique and self-contained elements within a domain model. They encapsulate attributes or characteristics but do not have an identity of their own, meaning they're defined by their attributes rather than an identifier. Examples of value objects could include the following:

  • Money (with currency and amount).
  • Date range (with start and end dates).
  • Address (with street, city and postal code).

Their immutability makes them suitable for ensuring consistency and preventing unexpected changes within the domain model. When changes are needed, new instances are created rather than modifying existing ones, preserving the integrity of the system.

Bounded context and distributed systems

While most of the bounded context advice holds equally true for monoliths and distributed services, separating services within a distributed system by bounded context is often the most logical way of approaching the system design. It also means that any identified aggregates are collocated, which helps avoid distributed transactions. However, if distributed transactions across bounded contexts are necessary, teams should consider implementing architecture patterns geared toward consistency, such as the saga pattern.

Aggregates

There are situations where the lifecycle and state of multiple objects are tightly bound. However, validating or rejecting changes to these objects in tandem is imperative to ensure consistency guarantees. In such instances, finding an encapsulating abstraction that can represent both objects is helpful. Once a team identifies this abstraction, they can change all objects within a single transaction by using this root aggregate. This guarantees the desired consistency across the domain.

An example of an aggregate in a system is an e-commerce system. In this system, an order can be an aggregate. An order typically includes elements like items, customer information, shipping address and payment details. Order is the main aggregate that ties everything together. It contains information about the order, like order ID, date and status. Customer information captures details about the customer placing the order. The shipping address is the address where the order should be delivered. Finally, payment details contain information about how a customer will pay for the order, including payment method, status, credit card details, and more.

In this scenario, the order acts as an aggregate root that holds together all these related pieces of information. The order is responsible for ensuring that all elements are consistent and adhere to the business rules (like ensuring the items are in stock, validating the payment method, etc.).

This grouping into an aggregate helps to manage complexity, ensures consistency and defines clear boundaries within the domain model.

Other object types

In addition to entities, value objects and aggregates, other object types like factories, services and repositories exist. These are common parts of a domain model framework, providing constructions, orchestration and state preservation capabilities.

Anti-patterns to watch out for

There are a few gotchas to watch out for when managing bounded contexts.

Anemic objects

Sometimes, objects in a domain model can seem legitimate on the surface: They have proper names and defined relationships. However, deeper investigation might reveal that these objects hold a consistent state but not consistent behavior. Identifying these "anemic" objects is one step toward simplifying the domain, as it will eliminate unnecessary abstractions from the model.

False cognates

A false cognate occurs when team members think they're talking about the same domain model concept but are not. As a result, the domain model might start to experience contradictory demands. It is important to clearly identify concepts, distinctions and expectations early in the design process to keep the domain model simple.

Duplicate concepts

Somewhat related to the last point, sometimes teams inadvertently create single concepts represented as two different models within the bounded context. They might have subtly differing rules, and it requires multiple domain models to be updated when one of them changes. It is important to identify them as duplicates to consolidate their behavior within the domain and reduce the overall complexity.

Code reuse at context boundaries

Reusing code between two bounded contexts can be tempting because the domain model seems to map one to one. This can be dangerous, though, as the domain model might evolve into different intents within their respective bounded context, which means the expectations from the domain model will differ and possibly conflict as each bounded context evolves. It is prudent to maintain the separation to keep the overall modeling clean and simple.

Priyank Gupta is a polyglot system architect who is well versed with the craft of building distributed systems that operate at scale. He is an active open source contributor, working on projects and libraries geared toward microservices development.

Dig Deeper on Enterprise architecture management