There isn’t any baked-in support for saga upcasting the way there is for events. We’ve taken two different approaches to this depending on the specifics of the change.
Easiest is to write a migration to update the saga data directly in the database by manipulating the XML or JSON. If you normally take your application down to deploy new code anyway, this may be the best option.
Harder is to migrate in several steps. Add migration code to the saga itself and make its fields backward-compatible at each step.
We prefer to avoid downtime if at all possible since our application exposes a public API that’s used 24x7, so we almost always use the second approach. In practice, this usually takes three code releases because we run multiple instances of our application and do rolling deploys, so during the deploy some instances are running code revision N and some are running N-1 and the two need to interoperate.
First, a bit of prep work, which only needs to be done once and can be reused across many migrations: Add a “migrate yourself” event type that’s handled by every saga class in your system. When any saga instance starts up, associate with a constant value that’s present on the event. We use the saga class name. The event handler can be empty in normal operation; you’ll only add code to it when you need to do a migration. Something like this:
`
public class SagaMigrationRequestedEvent {
private String className;
public SagaMigrationRequestedEvent(String className) { this.className = className; }
public String getClassName() { return className; }
}
public class MySaga {
…
@StartSaga
public void on(MySagaStartEvent event) {
…
associateWith(“className”, getClass().getName());
}
@SagaEventHandler(associationProperty = “className”)
public void on(SagaMigrationRequestedEvent event) {
// This will normally be empty
}
}
`
With that in place, migrations follow a sequence along these lines.
Release 1. Add the new fields, but make them null by default so they aren’t included in the serialized representation of the saga objects. If any fields you’re getting rid of are primitives (int, long, etc.) change them to boxed values (Integer, Long, etc.) Add code such that if the new fields are set, they’re used, but otherwise the old fields are used. Always write the old fields.
Release 2. Add code to the migration event handler to migrate data from the old fields to the new ones and then set the old fields to null. Change the saga to start writing the new fields instead of the old fields.
Once release 2 is running everywhere, publish the migration event so all existing saga instances will be updated to only have values in the new fields.
Release 3. Remove the old fields and the migration code.
You can safely roll back release 1 without any ill effects. You can roll back release 2 to release 1 but not to the version before release 1. You can roll back release 3 to release 2. If you want to roll back further than that, you’ll need to be able to migrate the saga data in the opposite direction. There’s pretty much no avoiding this since sagas are mutable, unlike events.
The approach you suggest (introducing an entirely new saga class) could also work, but it means you’ll have a mix of two separate saga classes running business logic at the same time, and you’ll have to keep the old code around for the lifetime of any existing saga instances. Whether or not that’s problematic obviously depends on your application. It has one potential gotcha if you’re rolling out releases across multiple instances of the application: an instance that is still running the old code (that lacks the new class) could encounter an event that has an association with an instance of the new class, and it would then fail to load the saga because the new class wouldn’t be found. Again, whether that’s a problem or not is up to your application. It’s only an issue if you do zero-downtime deploys; if you take the application completely down to move to new code releases, you’d never hit that problem.
-Steve