Being Successful with Domain-Driven Design: Minimal Complexity, Part 3

Published on Monday, June 12, 2023


With a name like "Domain-Driven Design," it should be no surprise there is a major focus on the domain and has a huge influence on implementation. We've focused mostly on strategic design patterns and practices like Ubiquitous Language, Bounded Context, etc. But I've also covered a bit of tactical design and implementation. I've transitioned from strategical patterns--that deal with being explicit with domain concepts (Ubiquitous Language, Bounded Contexts)--to tactical patterns that have focused on directly translating domain concepts into code structure or coding patterns (like Services and Aggregates.)

The concepts and their consistency boundaries are only a couple of things that contribute to the complexity of non-trivial domains. For example, the work required to implement a domain is independent of its concepts and consistency boundaries. Additionally, the system's quality attributes and technical constraints are major influencers on the internal structure of that system. The next set of Domain-Driven Design patterns I'll get into (tactical patterns) aid in this respect. As we get closer to implementation, the focus turns more towards isolating domain complexity from implementation complexities.

As with many things in Domain-Driven Design, x for the sake of x is not the intention. Many things in most methodologies can be regurgitated and used by rote, providing little to no value. The principles and practices in Domain-Driven Design are best utilized with purpose and intent. Architectural layering is a good example. Each layer needs a reason for being (a purpose) with unidirectional independence of its concepts from another layer's concepts. Just any two groupings won't do; without the purposeful intent of having two layers with a unidirectional dependency, you'll never gain the benefits of layering. You end up with the added burden of managing a structure that does not give you any layered benefits.

The minimum complexity for layers is that two groups of concepts (contexts) are uni-directionally interdependent. In Domain-Driven Design, those layers focus on isolating the concerns of a User Interface, Application, Domain, and Infrastructure.

The Application layer may seem unique to Domain-Driven Design. There are few patterns/methodologies that isolates the concern that the application layer deals with. Ports and Adapters (Hexagonal) and, by extension, Clean Architecture recognize and isolate high-level use cases from both the domain and the implementation details of a UI. This is the role of the Application layer in Domain-Driven Design to further isolate the domain from how any use case uses the domain (a use case applies or realizes the domain). In Ports and Adapters, uses-cases (or, as Cockburn describes, uses-cases) are sequences of interactions between the system and users/actors. With the recognition of these interactions, they can now be isolated as collaborations within the Application layer.

There are other patterns that isolate interaction behavior structurally within collaborations like the Adapter pattern, but I'll save that for another day.

A UI may have to deal with different form factors, communication protocols, execution contexts, etc. A loosely coupled UI involves designing an interface that takes all of those things into consideration to be successful. A web-based UI requires a backend that supports open protocols and standards. Protocols and standards relating to implementation or delivery are merely constraints on how a system is implemented. At some level, the domain needs to operate correctly regardless of those constraints.

Layers are like different team roles, all working together simultaneously to accomplish specific types of goals. Bounded contexts are also like multiple teams, sometimes like a night shift and a day shift or an on-shore and an off-shore team. These types of teams work with some level of independence: shifts may never work together simultaneously, and on- and off-shore teams only work together for a brief time with much more structured communication.

Recognizing and planning for how teams contribute to the same goals is key for these teams to be effective. It's the recognition that different parts of a larger system need different levels of independence. With teams, this is to utilize resources effectively: like how shifts can use limited resources (human skills) across more of the day (e.g., 24 hours instead of 8.) Conway's law is just an observation (i.e., a reality). A team (or teams) structure imposes a means and cadence to communication. How often and the way inter-team communications occurs has implicit limits on that communication.

Recognizing and working with that communications structure can make teams much more successful. Domain-Driven Design makes domains and sub-domains first-class citizens within the practices. Many aspects of architectural and social boundaries can affect the release of a product. A Bounded Context is more than a consistency boundary or scope of a domain model. A Bounded Context also involves work products (deployments, deliverables) and team organization.

For example, the consistency boundary of a mortgage loan application becoming complete and submitted is a fairly obvious boundary and context. Still, the amount of work involved to support that might be fairly large. The number of people implementing and supporting that context might amount to several teams. The complexity of dealing with several teams of people to deliver parts of the same system can be enormous. Domain-Driven Design also gives us some patterns and practices to address those complexities. You may need to split a domain into more bounded contexts because one context is too complex for a single team to manage. When we start to talk about separating work across teams, we're still talking about bounded contexts. For similar reasons, you may need to split a domain into more bounded contexts (and thus "sub-domains") simply because of an existing team or reporting structure.

For delivery to be more successful, it's important to recognize the different teams, reporting structures, team motivations, and missions within the strategic design of the Bounded Contexts. How two teams and how the work product of those two teams interact is unique. Fortunately, there are some patterns to address the dependencies between two teams and their work products that help us address their inherent complexities. I'm assuming there's always some degree of interdependency and independence, and I'm ignoring mutually independent (Separate Ways) and Big Ball of Mud relationships/structures.

It's worth noting that as soon as two contexts are recognized the need to translate between the two becomes a reality. As context become complex to the point of being bounded context so too does the need to recognize and isolate translation. Much of what we do in Domain-Driven Design is the isolation of concepts, concerns, responsibilities, etc. The need for a translation layer is no different.

There is a spectrum to the degree of independence of two teams and or the independence of their work products. The teams are very dependent on one end, while the other is extremely independent. With Domain-Driven Design, very dependent teams exhibit a lot of domain overlap. With a lot of domain overlap, you can have an interdependence where teams work as equals or partners. This partnership can manifest in an early re-org of people working on existing or legacy systems. That partnership may start with different teams working on separate parts of the codebase. This partnership may only be one step in the evolution of the teams; the next step is often to organize teams toward the Shared Kernel model.

A spectrum of options is a synonym for infinite combinations. It's nice to have flexibility, but an endless set of possibilities is hard to map to a finite set of patterns, and it's hard to use established practices if every situation is novel. There are some ideas and structures that Domain-Driven Design details to add some granularity to the domain we're modeling so that we can more easily map complexity to the patterns and practices that address them.

In the Shared Kernel model, the team carves off a separate shared codebase or a shared component to contain all the things that two or more teams will always or almost always mutually require. A Shared Kernel model involves organizational behavior, like specific responsibilities, code areas, accountability, etc. But Shared Kernel is a fairly casual relationship. With more formality between two teams or components, you usually see an Upstream/Downstream relationship form. It is easy to view the users of the Shared Kernel downstream dependents and evolve to a more formal Upstream/Downstream relationship. A Customer/Supplier model may emerge in cases like this.

A shared codebase is more casual than a shared component, but a shared component promotes more independence. At the component level, it's important to ensure that autonomy hasn't allowed the teams to deviate from a shared plot, which Continuous Integration is intended to address. The component should be integrated with client code at every opportunity. The intent of a Published Language is for all contexts to be on the same page in understanding that domain. It's not that all contexts will adopt the published language as their domain, but they know how to translate in and out of their domain.

In the Customer/Supplier model, one team owns a component the other uses as the consumer of the component's capabilities. The team that owns the component is the Supplier, and the team that uses it is the Customer. With this model comes organizational behavior with more specific responsibilities, more planning, and scheduling. With this increased independence, the supplier team has very specific goals in which the customer team has a stake and influence, represented in a release cadence and a roadmap. The Customer is usually the driver of what capabilities the component provides next. The integration model of Customer/Supplier is usually a web service.

In a Upstream/Downstream model, there will almost always be some form of Published Language--usually more formal than just a description, often a specification. Translation becomes more formal in a Upstream/Downstream model, often resulting in a translation layer. If the Upstream/Downstream relationship is between two Bounded Contexts with a high degree of independence an Anticorruption Layer is used on one side to manage the differences communicating between the two domains.

Shared kernel and two-team customer/supplier relationships often exist due to the reporting structure or that reporting structure was created to split work across two teams. In a more product-focused organization, you may have a Customer/Supplier model with more than one customer. Multi-customer relationships can be witnessed in larger organizations with things like shared libraries. The customers are still driving the capabilities that the component and team provide them, but it can become more formal to manage the unique requirements of different customers. The supplier team is often more organized or formal and may have more of a product strategy with a product vision and mission that helps guide their work.

With more formal relationships come more formal expectations. Those expectations may come in the form of specifications and processes. Continuous Integration is an example of a process that continuously validates integrability. Potentially less formal than a specification may be a Published Language--which in its simplest form is a description of the concepts of a domain. (more complex forms would be varying degrees of specifications.)

Communications with a customer/supplier model within the same organization can be informal. What the team is working on and how they interact with customers might be more like partnerships; teams may work closely together to implement and integrate components. The number of customers or distance from customers can impact this informalness. The further away a customer is (different division, different organization, different company) may impose more formality to the relationship. The work product of the supplier team may be viewed more like a product. And while customers may drive that product, it may be much more formal to the point where the component is independent of any single customer. This type of relationship may be structured more like a service with a very specific or well-specified interface. Moving towards a well-specified interface is the intent of an Open Host Service where the component is remotely accessed (a service) with a specified protocol and interface.

As a customer has less influence on a service, they may become completely dependent on the supplier team to provide the capabilities they require. They accept the risk that the supplier team may not provide the necessary capabilities in the future. This is extreme, and either no organization would accept this risk, or it is a temporary relationship. In reality, the different models aren't mutually exclusive, but there is a tendency towards one of them. e.g., a relationship tends to be less like a customer/supplier relationship and more like one completely conforming to another. The recognition of this relationship is called the Conformist model.

highlight: Recognize change will happen but don't try to create a design that accommodates all change.

comments powered by Disqus