One of the projects I work in has an uncommon tech stack, at least it differs from the standard CRUD-application-tendency many company-apps are built in. The mentioned application uses the Axon framework and a Mongo database. At first, the data often changed structurally, hence storing it in a document database accelerated development. Feature-requests for accessing the history of data encouraged the use of Axon with its events that are stored permanently in the database. Now, a few months down the road, data structures became more stable and more features have to be added to the application. For that, a lot of refactoring was planned. This is where the problems began.
To clarify, “refactoring” in this case means “moving classes to other packages”. A no-brainer in Java that does not cause problems. In our project however, this was simply not possible: Axon serializes events to Mongo via XStream. The serialized content contains the fully qualified name of the Java class of the event, including its package. The planned refactoring would break the serialization of old events, existing data could not be mapped back to Java classes. To not be stuck forever with the first version of our package structure, we researched the following options.
Option 1: Don’t Refactor
The easiest way to deal with the situation is to keep the package of classes. Forever. Obviously, this is not the best option because it means to stay with the very first package structure that was established at a very early time. Applications under development should be free to change architectural decisions and structure.
Option 2: Mongock
The direct and naive approach to “wrong” data in the database is to change the data in the database. For Mongo, this can be done with Mongock. Just like database versioning tools like Liquibase and Flyweight, changesets are created programmatically and applied to all documents. The tool itself seems to be ready for production and would solve our problem. We would simply write changesets for package changes caused by a refactoring.
It would work, however it would add much complexity to the project. Additionally, all developers in the team would have to keep in mind adding a changeset when refactoring. Forgetting this would have severe consequences: During our research, we noticed that mismatched package information (such as after refactoring) does not cause an error at application server startup. Axon simply logs an error message, stating that old data cannot be read, and goes on loading only the readable data. In my opinion, this is pretty bad. Who is reading the log of a server at startup? There’s a huge change that sometime in the future somebody will forget to write a changeset, causing old data to be unreadable.
Furthermore, the act of changing the events Axon wrote in the Mongo database is discouraged by the framework itself. Axon as an event store is considered a read and append-only data source, “history may not be rewritten” (see this article).
Because of this reasons, Mongock cannot be used in our situation.
Option 3: Event Upcasting
It was a great relief to read about event upcasting, also known as event transformer or event adapter. Upcaster are natively available in Axon and map a certain version of an event to another version. They are called when reading old events from the database, mapping them to a new version of the event, and forward them to be processed. That way, old events are not changed and the history stays the same.
Using upcasters would mean one upcaster for every changed class. For projects with some months or years of active developing, including refactoring, this means a huge number of upcasters and many different versions of data in the database. Although upcasters would solve our problem, these different versions are a problem. During analysis or refactoring, it would be hard to mentaly link contents in the database to the codebase. A developer would have to keep in mind all the upcasters used.
Huge refactorings, for example changing the root package, would mean to write one upcaster for every single event class. Refactorings such as this would cause an explosion of complexity.
Because of the mentioned reasons, using upcasters is not an option.
Option 4: Versioned Event Store
There are approaches to copy and modifiy an existing event store. However, this would mean much boilerplate actions to do for a simple refactoring, so this is not an option, too.
Option 5: Package Aliasing (XStream)
Our specific refactoring-caused problem can be approached one level below Axon. The framework uses XStream to serialize objects to Mongo, hence methods provided by XStream can be used. The most promising option is package aliasing. Basically, these aliases are another form of event upcasters. Although using aliases would mean less boiler plate, it would mean a lot of additionally work and complexity and would bring the same disadvantages like upcasters.
After having reviewed all the above actions, we concluded that using Axon does not provide sufficient freedom of action to refactor our codebase as we want to. We initiated a project to remove Axon from the project and migrate to a relational database.