Published on 01 June 2019 by @mathiasverraes
Explicitly conform to the legacy component’s model in an eventsourcing migration.
We are replacing a Legacy component with a new Eventsourced component. Besides being fully Eventsourced, the new component has a clear domain model with a well-defined Ubiquitous Language. The domain events are granular, and named in this language, and track the entire history of the component.
The new component needs to retain and use data produced by the old component. The old component stores state, but no history. There is not enough information in that state to deduce all the domain events reproduce the real history. In other words, we know the state but not how we got there. There is no way for new component to have a real complete history from the old component.
One solution is to keep the database of the Legacy component, mark it as read-only, and query it from the Eventsourced system. The downside is that the Eventsourced component is now a hybrid, and relies on two entirely different persistence strategies.
Another common solution is to make a best effort to force the data from the legacy component, into the domain events of the Eventsourced component. The appeal of this approach is that we have a single, straightforward way of doing things: one canonical model, that fits everything, and therefore easy to work with and reason about. The downside is that the domain events are lying. They didn’t happen in that way, we forced them into the new model. We’re hiding essential complexity, which often causes accidental complexity as a side-effect.
That said, both solutions may be perfectly acceptable, especially if the underlying conceptual models for the legacy and new component are very close.
“The old system is now the new problem.”
The mindshift is to see the old system as a domain. The new system is not only solving for the business domain, but for the old system’s domain as well. If the old system is az domain, we can apply domain modelling to it, draw a Bounded Context around it, agree on a Ubiquitous Language, and reason about its relation to other Bounded Contexts.
Migration Events are now domain events in the new component, but they use the Ubiquitous Language of the old component. They’re not shy about the fact that they are Migration Events, and they don’t try to pretend they happened in the new component. Typical Migration Events names are
LegacyOrderWasPlaced. If concepts existed in the Legacy component, that do not exist in the new component, the Migration Events reflect those legacy concepts. No attempt is made to deduce more history than what is available in the Legacy component.
This may sound more complex than the alternatives: we have more event types, that we are stuck with forever. All our code, such as projections, will need to deal with both the new event types and with the Migration Events. But by hiding the fact the we did have a Legacy component, we are hiding complexity and end up with a system that is harder to understand for people who where not involved in the old component.
In DDD, we express the domain in the design, it’s just not always obvious what the domain is. Bounded Contexts are essentially about lifting a domain model and its language into a first-class concept in our designs. By treating a model+language as a unit, we can do higher-order reasoning about them: replace models, protect against models, find patterns for relationships between models. In our case, the new Bounded Context is Conformist to the Legacy Bounded Context. The Conformist pattern has an unfortunate negative connotation, but is actually a good strategy when applied judiciously.
Even after we delete the Legacy component, the Legacy Bounded Context stays alive. It just isn’t tied to a physical component anymore. The language and model exist, but the implementation has disappeared. But it is still a Bounded Context: there’s language in there, like
LegacyCustomerWasImported, and it is internally consistent to a model, even if most of that model has no implementation anymore. (Note that the
LegacyCustomerWasImported did not exist in the old model, but it represents a
Customer as it was defined in that model.) This approach of seeing Bounded Contexts as first-class concepts, autonomous from implementations, is one of the superpowers of Domain-Driven Design in my opinion, and one of its most unique innovations.
I propose Ghost Context as the pattern name for a legacy Bounded Context with no implementation, but that still has traces in other Bounded Contexts.
“The ghost of the old system continues to haunt the new.”
Follow @mathiasverraes on Twitter.
This work by Mathias Verraes is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 License.