Chapter 14: Maintaining Model Integrity
- Shares a story of one group trying to reuse the code of a Charge module written by a different group, and how that started breaking that other group's code.
- "The problem was that these two groups had different models, but they did not realize it, and there were no processes in place to detect it. Each made assumptions about the nature of a charge that were useful in their context (billing customers versus paying vendors). When their code was combined without resolving these contradictions, the result was unreliable software."
- "If only they had been more aware of this reality, they could have consciously decided how to deal with it. That might have meant working together to hammer out a common model and then writing an automated test suite to prevent future surprises. Or it might simply have meant an agreement to develop separate models and keep hands off each other’s code. Either way, it starts with an explicit agreement on the boundaries within which each model applies."
- "[T]he most fundamental requirement of a model is that it be internally consistent; that its terms always have the same meaning, and that it contain no contradictory rules. The internal consistency of a model, such that each term is unambiguous and no rules contradict, is called unification. A model is meaningless unless it is logically consistent."
- "To maintain that level of unification in an entire enterprise system is more trouble than it is worth. It is necessary to allow multiple models to develop in different parts of the system, but we need to make careful choices about which parts of the system will be allowed to diverge and what their relationship to each other will be. We need ways of keeping crucial parts of the model tightly unified. None of this happens by itself or through good intentions. It happens only through conscious design decisions and institution of specific processes. Total unification of the domain model for a large system will not be feasible or cost-effective"
- "This chapter lays out techniques for recognizing, communicating, and choosing the limits of a model and its relationships to others. It all starts with mapping the current terrain of the project. A bounded context defines the range of applicability of each model,
while a context map gives a global overview of the project’s contexts and the relationships between them. This reduction of ambiguity will, in and of itself, change the way things happen on the project,
but it isn’t necessarily enough. Once we have a context bounded,
a process of continuous integration will keep the model unified.
Then, starting from this stable situation, we can start to migrate toward more effective strategies for bounding contexts and relating them, ranging from closely allied contexts with shared kernels to loosely coupled models that go their separate ways." - Bounded Context
- "Multiple models are in play on any large project. Yet when code based on distinct models is combined, software becomes buggy, unreliable, and difficult to understand. Communication among team members becomes confused. It is often unclear in what context a model should not be applied."
- "Explicitly define the context within which a model applies. Explicitly set boundaries in terms of team organization, usage within specific parts of the application, and physical manifestations such as code bases and database schemas. Keep the model strictly consistent within these bounds, but don’t be distracted or confused by issues outside."
- Recognizing Splinters Within a Bounded Context
- "Combining elements of distinct models causes two categories of problems: duplicate concepts and false cognates."
- "Duplication of concepts means that there are two model elements (and attendant implementations) that actually represent the same concept. Every time this information changes, it has to be updated in two places with conversions. Every time new knowledge leads to a change in one of the objects, the other has to be reanalyzed and changed too. Except the reanalysis doesn’t happen in reality, so the result is two versions of the same concept that follow different rules and even have different data. On top of that, the team members must learn not one but two ways of doing the same thing, along with all the ways they are being synchronized."
- "False cognates may be slightly less common, but more insidiously harmful. This is the case when two people who are using the same term (or implemented object) think they are talking about the same thing, but really are not. [...] False cognates lead to development teams that step on each other’s code, databases that have weird contradictions, and confusion in communication within the team."
- "When you detect these problems, your team will have to make a decision. You may want to pull the model back together and refine the processes to prevent fragmentation. Or the fragmentation may be a result of groups who want to pull the model in different directions for good reasons, and you may decide to let them develop independently. Dealing with these issues is the subject of the remaining patterns in this chapter."
- Continuous Integration
- "Having defined a bounded context, we must keep it sound."
- "When a number of people are working in the same bounded context, there is a strong tendency for the model to fragment. The bigger the team, the bigger the problem, but as few as three or four people can encounter serious problems. Yet breaking down the system into ever-smaller contexts eventually loses a valuable level of integration and coherency."
- "Continuous integration means that all work within the context is being merged and made consistent frequently enough that when splinters happen they are caught and corrected quickly. continuous integration, like everything else in domain-driven design, operates at two levels: (1) the integration of model concepts and (2) the integration of the implementation."
- "Concepts are integrated by constant communication among team members. The team must cultivate a shared understanding of the ever-changing model. Many practices help, but the most fundamental is constantly hammering out the ubiquitous language. Meanwhile, the implementation artifacts are being integrated by a systematic merge/build/test process that exposes model splinters early. Many processes for integration are used, but most of the effective ones share these characteristics:"
- A step-by-step, reproducible merge/build technique
- Automated test suites
- Rules that set some reasonably small upper limit on the lifetime of unintegrated changes
- "Institute a process of merging all code and other implementation artifacts frequently, with automated tests to flag fragmentation quickly. Relentlessly exercise the ubiquitous language to hammer out a shared view of the model as the concepts evolve in different people’s heads."
- Context Map
- "An individual bounded context still does not provide a global view. The context of other models may still be vague and in flux."
- "People on other teams won’t be very aware of the context bounds and will unknowingly make changes that blur the edges or complicate the interconnections. When connections must be made between different contexts, they tend to bleed into each other."
- "Identify each model in play on the project and define its bounded context. This includes the implicit models of nonobject-oriented subsystems. Name each bounded context, and
make the names part of the ubiquitous language.
Describe the points of contact between the models, outlining explicit translation for any communication and highlighting any sharing.
Map the existing terrain. Take up transformations later." - Testing at the Context Boundaries
- "Contact points with other bounded contexts are particularly important to test. Tests help compensate for the subtleties of translation and the lower level of communication that typically exist at boundaries. They can act as a valuable early warning system, especially reassuring in cases where you depend on the details of a model you don’t control."
- Organizing and Documenting Context Maps
- "The bounded contexts should have names so that you can talk about them. Those names should enter the ubiquitous language of the team."
- "Everyone has to know where the boundaries lie, and be able to recognize the context of any piece of code or any situation."
- Relationships Between Bounded Contexts
- "The following patterns cover a range of strategies for relating two models that can be composed to encompass an entire enterprise. These patterns serve the dual purpose of providing targets for successfully organizing development work, and supplying vocabulary for describing the existing organization."
- Shared Kernel
- "Uncoordinated teams working on closely related applications can go racing forward for a while, but what they produce may not fit together. They can end up spending more on translation layers and retrofitting than they would have on continuous integration in the first place, meanwhile duplicating effort and losing the benefits of a common ubiquitous language."
- "Designate some subset of the domain model that the two teams
agree to share. Of course this includes, along with this subset of the model, the subset of code or of the database design associated with
that part of the model. This explicitly shared stuff has special status,
and shouldn’t be changed without consultation with the other team.
Integrate a functional system frequently, but somewhat less often than the pace of continuous integration within the teams. At these integrations, run the tests of both teams." - Customer/Supplier Development Teams
- "Often one subsystem essentially feeds another; the “downstream” component performs analysis or other functions that feed back very little into the “upstream” component, and all dependencies go one way. The two subsystems commonly serve very different user communities, who do different jobs, where different models may be useful. The tool set may also be different, so that program code cannot be shared."
- "The freewheeling development of the upstream team can be cramped if the downstream team has veto power over changes, or if procedures for requesting changes are too cumbersome. The upstream team may even be inhibited, worried about breaking the downstream system. Meanwhile, the downstream team can be helpless, at the mercy of upstream priorities."
- "Establish a clear customer/supplier relationship between the
two teams. In planning sessions, make the downstream team play
the customer role to the upstream team. Negotiate and budget tasks
for downstream requirements so that everyone understands the
commitment and schedule.
Jointly develop automated acceptance tests that will validate the interface expected. Add these tests to the upstream team’s test suite, to be run as part of its continuous integration. This testing will free the upstream team to make changes without fear of side effects downstream." - There are two crucial elements to this pattern
- " The relationship must be that of customer and supplier, with the implication that the customer’s needs are paramount. Because the downstream team is not the only customer, the different customers’ demands have to be balanced in negotiation—but they remain priorities. This situation is in contrast to the poor-cousin relationship that often emerges, in which the downstream team has to come begging to the upstream team for its needs."
- "There must be an automated test suite that allows the upstream team to change its code without fear of breaking the downstream, and lets the downstream team concentrate on its own work without constantly monitoring the upstream team."
- Conformist
- "When two development teams have an upstream/downstream relationship in which the upstream has no motivation to provide for the downstream team’s needs, the downstream team is helpless. Altruism may motivate upstream developers to make promises, but they are unlikely to be fulfilled. Belief in those good intentions leads the downstream team to make plans based on features that will never be available. The downstream project will be delayed until the team ultimately learns to live with what it is given. An interface tailored to the needs of the downstream team is not in the cards."
- "In this situation, there are three possible paths."
- Separate Ways - "One is to abandon use of the upstream altogether. This option should be evaluated realistically, making no assumptions that the upstream will accommodate downstream needs."
- Anticorruption Layer - "If the design is very difficult to work with, perhaps for lack of encapsulation, awkward abstractions, or modeling in a paradigm the team cannot use, then the downstream team will still need to develop its own model. They will have to take full responsibility for a translation layer that is likely to be complex."
- Conformist - "On the other hand, if the quality is not so bad, and the style is reasonably compatible, then it may be best to give up on an independent model altogether."
- "Eliminate the complexity of translation between bounded contexts by slavishly adhering to the model of the upstream team. Although this cramps the style of the downstream designers and probably does not yield the ideal model for the application, choosing conformity enormously simplifies integration. Also, you will share a ubiquitous language with your supplier team. The supplier is in the driver’s seat, so it is good to make communication easy for them. Altruism may be sufficient to get them to share information with you. "
- Anticorruption Layer
- "When a new system is being built that must have a large interface with another, the difficulty of relating the two models can eventually overwhelm the intent of the new model altogether, causing it to be modified to resemble the other system’s model, in an ad hoc fashion. The models of legacy systems are usually weak, and even the exception that is well developed may not fit the needs of the current project. Yet there may be a lot of value in the integration, and sometimes it is an absolute requirement."
- "Create an isolating layer to provide clients with functionality in terms of their own domain model. The layer talks to the other system through its existing interface, requiring little or no modification to the other system. Internally, the layer translates in both directions as necessary between the two models."
- Designing the Interface of the Anticorruption Layer
- "The public interface of the anticorruption layer usually appears as a set of services, although occasionally it can take the form of an entity. Building a whole new layer responsible for the translation between the semantics of the two systems gives us an opportunity to reabstract the other system’s behavior and offer its services and information to our system consistently with our model. It may not even make sense, in our model, to represent the external system as a single component. It may be best to use multiple services (or occasionally entities), each of which has a coherent responsibility in terms of our model."
- Implementing the Anticorruption Layer
- "One way of organizing the design of the anticorruption layer is as a combination of facades, adapters (both from Gamma et al. 1995), and translators, along with the communication and transport mechanisms usually needed to talk between systems."
- "We often have to integrate with systems that have large, complicated, messy interfaces. [...] Translating from one model to another (especially if one model is fuzzy) is a hard enough job without simultaneously dealing with a subsystem interface that is hard to talk to. Fortunately, that is what facades are for. A facade is an alternative interface for a subsystem that simplifies access for the client and makes the subsystem easier to use."
- "An adapter is a wrapper that allows a client to use a different protocol than that understood by the implementer of the behavior. When a client sends a message to an ADAPTER, it is converted to a semantically equivalent message and sent on to the “adaptee.” The response is converted and passed back. I’m using the term adapter a little loosely, because the emphasis in Gamma et al. 1995 is on making a wrapped object conform to a standard interface that clients expect, whereas we get to choose the adapted interface, and the adaptee is probably not even an object. Our emphasis is on translation between two models, but I think this is consistent with the intent of adapter."
- "The remaining element is the translator. The adapter's job is to know how to make a request. The actual conversion of conceptual objects or data is a distinct, complex task that can be placed in its own object, making them both much easier to understand. A translator can be a lightweight object that is instantiated when needed. It needs no state and does not need to be distributed, because it belongs with the adapters(s) it serves."
- A Cautionary Tale
- Separate Ways
- "Integration is always expensive. Sometimes the benefit is small."
- "Declare a bounded context to have no connection to the others at all, allowing developers to find simple, specialized solutions within this small scope."
- Open Host Service
- "When a subsystem has to be integrated with many others, customizing a translator for each can bog down the team. There is more and more to maintain, and more and more to worry about when changes are made."
- "Define a protocol that gives access to your subsystem as a set of services. Open the protocol so that all who need to integrate with you can use it. Enhance and expand the protocol to handle new integration requirements, except when a single team has idiosyncratic needs. Then, use a one-off translator to augment the protocol for that special case so that the shared protocol can stay simple and coherent."
- Published Language
- "When businesses want to exchange information with one another, how do they do it? Not only is it unrealistic to expect one to adopt the domain model of the other, it may be undesirable for both parties. A domain model is developed to solve problems for its users; such a model may contain features that needlessly complicate communication with another system. Also, if the model underlying one of the applications is used as the communications medium, it cannot be changed freely to meet new needs, but must be very stable to support the ongoing communication role."
- "Direct translation to and from the existing domain models may not be a good solution. Those models may be overly complex or poorly factored. They are probably undocumented. If one is used as a data interchange language, it essentially becomes frozen and cannot respond to new development needs."
- "Use a well-documented shared language that can express the necessary domain information as a common medium of communication, translating as necessary into and out of that language."
- Unifying an Elephant
- Starts off by quoting parts of "The Blind Men and the Elephant," by John Godfrey Saxe, where each blind man compares the elephant to something different depending on what part they touched: a wall, a snake, a tree, a rope, etc.
- "Depending on their goals in interacting with the elephant, the various blind men may still be able to make progress, even if they don’t fully agree on the nature of the elephant. If no integration is required, then it doesn’t matter that the models are not unified. If they require some integration, they may not actually have to agree on what an elephant is, but they will get a lot of value from merely recognizing that they don’t agree. This way, at least they don’t unknowingly talk at cross-purposes."
- "As the blind men want to share more information about the elephant, the value of sharing a single bounded context goes up. But unifying the disparate models is a challenge. None of them is likely to give up his model and adopt one of the others. After all, the man who touched the tail knows the elephant is not like a tree, and that model would be meaningless and useless to him. Unifying multiple models almost always means creating a new model."
- "With some imagination and continued discussion (probably heated), the blind men could eventually recognize that they have been describing and modeling different parts of a larger whole. For many purposes, a part-whole unification may not require much additional work. At least the first stage of integration only requires figuring out how the parts are related. It may be adequate for some needs to view an elephant as a wall, held up by tree trunks, with a rope at one end and a snake at the other."
- "Matters are more difficult when two models are looking at the same part in a different way. If two men had touched the trunk and one described it as a snake and the other described it as a fire hose, they would have had more difficulty. Neither can accept the other’s model, because it contradicts his own experience. In fact, they need a new abstraction that incorporates the “aliveness” of a snake with the water-shooting functionality of a fire hose, but one that leaves out the inapt implications of the first models, such as the expectation of possibly venomous fangs, or the ability to be detached from the body and rolled up into a compartment in a fire truck."
- "Even though we have combined the parts into a whole, the resulting model is crude. It is incoherent, lacking any sense of following contours of an underlying domain. New insights could lead to a deeper model in a process of continuous refinement. New application requirements can also force the move to a deeper model. If the elephant starts moving, the “tree” theory is out, and our blind modelers may break through to the concept of “legs.”"
- "This second pass of model integration tends to slough off incidental or incorrect aspects of the individual models and creates new concepts—in this case, “animal” with parts “trunk,” “leg,” “body,” and “tail”—each of which has its own properties and clear relationships to other parts. Successful model unification, to a large extent, hinges on minimalism. An elephant trunk is both more and less than a snake, but the “less” is probably more important than the “more.” Better to lack the water-spewing ability than to have an incorrect poison-fang feature."
- Choosing Your Model Context Strategy
- "It is important always to draw the CONTEXT MAP to reflect the current situation at any given time. Once that’s done, though, you may very well want to change that reality. Now you can begin to consciously choose CONTEXT boundaries and relationships. Here are some guidelines."
- Team Decision or Higher
- "Teams have to make these decisions, or at least the decisions have to be propagated to the entire team and understood by everyone. In fact, such decisions often involve agreements beyond your own team. On the merits, decisions about whether to expand or to partition bounded contexts should be based on the cost-benefit trade-off between the value of independent team action and the value of direct and rich integration. In practice, political relationships between teams often determine how systems are integrated"
- Putting Ourselves in Context
- "When we are working on a software project, we are interested primarily in the parts of the system our team is changing (the “system under design”) and secondarily in the systems it will communicate with. [...] We really are part of that primary context we are working in, and that is bound to be reflected in our context. This isn’t a problem if we are aware of the bias and are mindful of when we step outside the limits of that map’s applicability."
- Transforming Boundaries
- "[T]ypically the struggle is to balance some subset of the following forces:"
- Favoring Larger Bounded Contexts
- Flow between user tasks is smoother when more is handled with a unified model.
- It is easier to understand one coherent model than two distinct ones plus mappings
- Translation between two models can be difficult (sometimes impossible).
- Shared language fosters clear team communication.
- Favoring Smaller Bounded Contexts
- Communication overhead between developers is reduced.
- Continuous integration is easier with smaller teams and code bases.
- Larger contexts may call for more versatile abstract models, requiring skills that are in short supply.
- Different models can cater to special needs or encompass the jargon of specialized groups of users, along with specialized dialects of the ubiquitous language.
- Accepting That Which We Cannot Change: Delineating the External Systems
- "Some subsystems will clearly not be in any BOUNDED CONTEXT of the system under development. Examples would be major legacy systems that you are not immediately replacing and external systems that provide services you’ll need. You can identify these immediately and prepare to segregate them from your design."
- "Here we must be careful about our assumptions.It is not unusual to find semantic contradictions in different parts of such systems."
- Relationships with the External Systems
- "There are three patterns that can apply here. First, to consider separate ways. Yes, you wouldn’t have included them if you didn’t need integration. But be really sure. Would it be sufficient to give the user easy access to both systems? Integration is expensive and distracting, so unburden your project as much as you can."
- "If the integration is really essential, you can choose between two extremes: conformist or anticorruption layer."
- The System Under Design
- "The software your project team is actually building is the system under design. You can declare bounded contexts within this zone and apply continuous integration within each to keep them unified. But how many should you have? What relationships should they have to each other? The answers are less cut and dried than with the external systems because we have more freedom and control."
- Catering to Special Needs with Distinct Models
- "Different groups within the same business have often developed their own specialized terminologies, which may have diverged from one another. These local jargons may be very precise and tailored to their needs. Changing them (for example, by imposing a standardized, enterprise-wide terminology) requires extensive training and analysis to resolve the differences. Even then, the new terminology may not serve as well as the finely tuned version they already had."
- "You may decide to cater to these special needs in separate bounded contexts, allowing the models to go separate ways, except for continuous integration of translation layers. Different dialects of the ubiquitous language will evolve around these models and the specialized jargon they are based on. If the two dialects have a lot of overlap, a shared kernel may provide the needed specialization while minimizing the translation cost."
- Deployment
- "Coordinating the packaging and deployment of complex systems is
one of those boring tasks that are almost always a lot harder than they
look. The choice of bounded context strategy has an impact on the
deployment. For example, when customer/supplier teams deploy
new versions, they have to coordinate with each other to release versions that have been tested together. Both code and data migrations
have to work in these combinations. In a distributed system, it may
help to keep the translation layers between contexts together within
a single process, so that you don’t have multiple versions coexisting.
Even deployment of the components of a single bounded context can be challenging when data migration takes time or when distributed systems can’t be updated instantaneously, resulting in two versions of the code and data coexisting." - The Trade-off
- "To sum up these guidelines, there is a range of strategies for unifying or integrating models. In general terms, you will trade off the benefits of seamless integration of functionality against the additional effort of coordination and communication. You trade more independent action against smoother communication. More ambitious unification requires control over the design of the subsystems involved."
- When Your Project Is Already Under Way
- "Most likely, you are not starting a project but are looking to improve a project that is already under way. In this case, the first step is to define BOUNDED CONTEXTS according to the way things are now."
- "Once you have delineated your true current bounded contexts and described the relationships they currently have, the next step is to tighten up the team’s practices around that current organization. Improve your continuous integration within the contexts. Refactor any stray translation code into your anticorruption layers. Name the existing bounded contexts and make sure they are in the ubiquitous language of the project."
- "Now you are ready to consider changes to the boundaries and relationships themselves. These changes will naturally be driven by the same principles I’ve already described for a new project, but they will have to be bitten off in small pieces, chosen pragmatically to give the most value for the least effort and disruption."
- Transformations
- "Like any other aspect of modeling and design, decisions about bounded contexts are not irrevocable. Inevitably, there will be many cases in which you have to change your initial decision about the boundaries and relationships between bounded contexts."
- Merging Contexts: Separate Ways -> Shared Kernel
- "This is hard to do. It’s not too late, but it takes some patience."
- "Evaluate the initial situation. Be sure that the two contexts are indeed internally unified before beginning to unify them with each other."
- "Set up the process. You’ll need to decide how the code will be shared and what the module naming conventions will be. There must be at least weekly integration of the shared kernel code. And it must have a test suite. Set this up before developing any shared code. (The test suite will be empty, so it should be easy to pass!)"
- "Choose some small subdomain to start with—something duplicated in both contexts, but not part of the core domain. This first merger is going to establish the process, so it is best to use something simple and relatively generic or noncritical. Examine the integrations and translations that already exist. Choosing something that is being translated has the advantage of starting out with a proven translation, plus you’ll be thinning your translation layer."
- "Form a group of two to four developers, drawn from both teams, to work out a shared model for the subdomain. Regardless of how the model is derived, it must be ironed out in detail. This includes the hard work of identifying synonyms and mapping any terms that are not already being translated. This joint team outlines a basic set of tests for the model."
- "Developers from either team take on the task of implementing the model (or adapting existing code to be shared), working out details and making it function. If these developers run into problems with the model, they reconvene the team from step 3 and participate in any necessary revisions of the concepts."
- "Developers of each team take on the task of integrating with the new shared kernel."
- "Remove translations that are no longer needed."
- Merging Contexts: Shared Kernel -> Continuous Integration
- "This is not just a matter of resolving the model differences. You are going to be changing team structures and ultimately the language people speak."
- "Be sure that all the processes needed for continuous integration (shared code ownership, frequent integration, and so on) are in place on each team, separately. Harmonize integration procedures on the two teams so that everyone is doing things in the same way."
- "Start circulating team members between teams. This will create a pool of people who understand both models, and will begin to connect the people of the two teams."
- "Clarify the distillation of each model individually."
- "At this point, confidence should be high enough to begin merging the core domain into the shared kernel. This can take several iterations, and sometimes temporary translation layers are needed between the newly shared parts and the not-yet-shared parts. Once into merging the core domain, it is best to go pretty fast. It is a high-overhead phase, fraught with errors, and should be shortened as much as possible, taking priority over most new development. But don’t take on more than you can handle."
- "As the shared kernel grows, increase the integration frequency to daily and finally to continuous integration."
- "As the shared kernel approaches the point of encompassing all of the two former bounded contexts, you will find yourself with either one large team or two smaller teams that have a shared code base that they integrate continuously, and that trade members back and forth frequently."
- Phasing Out a Legacy System
- "Identify specific functionality of the legacy that could be added to one of the favored systems within a single iteration."
- " Identify additions that will be required in the anticorruption layer."
- "Implement."
- "Deploy."
- "Identify any unnecessary parts of the anticorruption layer and remove them."
- "Consider excising the now-unused modules of the legacy system, though this may not turn out to be practical. Ironically, the better designed the legacy system is, the easier it will be to phase it out. But badly designed software is hard to dismantle a little at a time. It may be possible to just ignore the unused parts until a later time when the remainder has been phased out and the whole thing can be switched off."
- Open Host Service -> Published Language
- " If an industry-standard language is available, evaluate it and use it if at all possible."
- "If no standard or prepublished language is available, then begin by sharpening up the core domain of the system that will serve as the host."
- "Use the core domain as the basis of an interchange language, using a standard interchange paradigm such as XML, if at all possible."
- " Publish the new language to all involved in the collaboration (at least)."
- "If a new system architecture is involved, publish that too."
- "Build translation layers for each collaborating system."
- "Switch over."