Aggregator Version Casting

All the applications will be updated by changing their versions from time to time. When a new version is deployed, the old version will be replaced by the new version (if you are not going to use service-mesh). But in the event-based architecture, some events can have been waiting to be executed when the new version is being deployed or after deployed as well.

Then the old event should be mapped with the new version. That means by using the old serialized aggregator objects that are in the event-store should be able to create a new aggregator object.

So, if you don’t consider the old version’s events that might be in the event-store, when developing the new version, the events will be conflicted or crashed while mapping the old event to the new aggregator version. Because the old version’s events are going to be executed by using the new aggregator.

Event rebuilding by the engine for transaction retrying.

stacksaga diagram event rebuilding

Aggregator-Oriented casting

If some changes are done for the aggregator, it will cause for a version-update. Even though the version is updated, there is something to be considered carefully. After updating the aggregator’s version, it is not a problem for that particular version at all. But StackSaga follows the event sourcing architecture and event re-invoking mechanism, in the event-store can have some events from the old version of the aggregator to be re-invoked. Therefore, the SEC has to cast (map/convert/deserialize) the old aggregator event to the new version. It is called as Aggregator-Oriented casting.

Even thought the term is quite simple at first glance, It can be effected to get crashed all the old events if you are unable to manage the version mapping. The casting process is failed for the old version’s events, there is no chance to be re-invoked that retry process with the old versions.
Even though the casting process is done by the SEC, as the developer, you are responsible for managing the casting the new version with the old versions' events.

Based on the behavior of the aggregator state-change, the Aggregator-Oriented version casting can be divided to two.

Aggregator-Oriented Up-Casting

If the new aggregator has more new attributes than the old one, that kind of event should be cast with upcasting.

For instance, the old version’s aggregator has 3 fields, and the new version can have 4 fields or more than that, the old version’s events should be cast to the new aggregator object, and that term is called as upcasting.

The following image shows how it is done by the SEC.

Stacksaga aggregator version up-casting

Explanation: In the event-store can have old remaining events to be re-tried. But due to the fact that the aggregator has been updated to a new version with new attributes, The old events also should be converted and should be run through the new aggregator. But if it is an upcast mapping, The new version doesn’t bother to the casting process at all due that SEC uses jackson objectmapper to serialization and deserialization both. The new values will have the default values with help of jackson objectmapper and you have nothing to worry about upcasting with StackSaga at all. See the <<,technical documentation>>

All the events that go through the SEO process can be identified easily in the Executor’s methods with the help of event-version data that provides with the aggregator object. See the [technical documentation]

Aggregator-Oriented Down-Casting

Stacksaga aggregator version down-casting

According to the example, the event-store might have the old events consisting of 2 fields. And if the new version has a less number of fields than the old one, it is called as down-casting. Now those remaining events are going to be executed through the new version. And the framework tries to build the new aggregator object by using the old event’s binaries. This is the time the casting is been worked on. You have to modify and annotate the aggregator so as not to conflict while building the object by the framework. Here you can see the best practices related to the casting of aggregators.

Related Topics

Aggregator’s Event Casting

As per the architecture documentation, StackSags does support Aggregator Event UpCasting and also Aggregator Event DownCasting.
For the upcasting of the Aggregator Event, there is nothing to be done. The only thing is adding the new properties. But the down-casting process is a little more complicated.

To overcome Aggregator’s Event down-casting in StackSaga, it can be followed in two different approaches technically.

  1. Ignore Unknown JSON Properties.

  2. Collect Missing properties

Ignore Unknown JSON Properties Is not a recommended approach. Technically, it is possible to ignore the missing properties in Object Mapper by using DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES globally or using @JsonIgnoreProperties(ignoreUnknown = true). Annotation in class level. But it is not recommended in StackSaga. Because old events have more data that the new one when it provided down-casting changes in your aggregator. Then, while the old events are processed, that removed data will not be available for accessing.

Collect Missing properties

For collecting missing properties it can be used @JsonAnySetter and @JsonAnyGetter with a custom method. But you don’t need to create all the things from scratch. Because StackSaga provides the helper class called MissingJsonPropertyCollector for extending without writing any custom methods.

The root implementation (Custom Aggregator) of the Aggregator is by default extended from MissingJsonPropertyCollector. Therefore, the root Custom Aggregator class is ready to collect the missing properties. But if you want to empower other inner classes that are used inside the Root Aggregator class also with Missing properties collector capabilities, you can extend all the inner classes from MissingJsonPropertyCollector as well.

Here is an example for down-casting with using MissingJsonPropertyCollector implementation.

  • Old Version Of the Custom Aggregator.

@SagaAggregator(
        version = @SagaAggregatorVersion(major = 1, minor = 1, patch = 0),
        idPrefix = "po",
        name = "PlaceOrderAggregator",
        sagaSerializable = PlaceOrderAggregatorSample.class,
        mapper = PlaceOrderAggregatorJsonMapper.class
)
@Getter
@Setter
public class PlaceOrderAggregator extends Aggregator {

    public PlaceOrderAggregator() {
        super(PlaceOrderAggregator.class);
    }

    @JsonProperty("order_id")
    private String orderId;

    @JsonProperty("username")
    private String username;

    @JsonProperty("total")
    private Double total;

    @JsonProperty("is_active")
    private Integer isActive;

    @JsonProperty("comment")
    private String comment;

    @JsonProperty("item_details")
    private List<ItemDetail> itemDetails = new ArrayList<>();

    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public static class ItemDetail implements Serializable {

        @JsonProperty("order_id")
        private String itemName;

        @JsonProperty("qty")
        private int qty;

        @JsonProperty("price")
        private double price;

        @JsonProperty("note")
        private String note;
    }
}

In this PlaceOrderAggregator class you can see some properties in the root class, and also there is another nested class called ItemDetail and it contains more properties regarding the items.

  • New Version Of the Custom Aggregator.

@SagaAggregator(
        version = @SagaAggregatorVersion(major = 1, minor = 1, patch = 1),
        idPrefix = "po",
        name = "PlaceOrderAggregator",
        sagaSerializable = PlaceOrderAggregatorSample.class,
        mapper = PlaceOrderAggregatorJsonMapper.class
)
@Getter
@Setter
public class PlaceOrderAggregator extends Aggregator {

    public PlaceOrderAggregator() {
        super(PlaceOrderAggregator.class);
    }

    @JsonProperty("order_id")
    private String orderId;

    @JsonProperty("username")
    private String username;

    @JsonProperty("total")
    private Double total;

    @JsonProperty("is_active")
    private Integer isActive;

    //@JsonProperty("comment") (1)
    //private String comment;

    @JsonProperty("item_details")
    private List<ItemDetail> itemDetails = new ArrayList<>();

    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public static class ItemDetail extends MissingJsonPropertyCollector { (3)

        @JsonProperty("order_id")
        private String itemName;

        @JsonProperty("qty")
        private int qty;

        @JsonProperty("price")
        private double price;

        //@JsonProperty("note") (2)
        //private String note;
    }
}

Relatively the old version, some attributes have been removed from the root class and also from the ItemDetail nested class. That means that the old event data should be cast down when it is deserialized into the new aggregator class.

1 The comment property has been removed from the root class. But should not be executed from the MissingJsonPropertyCollector. Because the root class is already executed from the MissingJsonPropertyCollector through the Aggregator class.
2 The note property has been removed from the ItemDetail class.
3 To be collected that missing property (note), the ItemDetail has been extended from the MissingJsonPropertyCollector class. Then the deserialization is happened that missing property will be saved in to the missingProperties map in side of teh MissingJsonPropertyCollector that has been provided by the framework.
If the ItemDetail has not been extended from the MissingJsonPropertyCollector class, an exception will be thrown by the framework when the application is started by mapping the old version’s samples that you have given in the previous version through the SagaSerializable implementation. It will ensure that the application is in a casting trouble.
  • Getting The Collected Properties For specific Version.

@SagaExecutor(executeFor = "order-service", liveCheck = true, value = "OrderSaveExecutor")
@AllArgsConstructor
public class OrderSaveExecutor implements CommandExecutor<PlaceOrderAggregator> {

    @Override
    public ProcessStepManager<PlaceOrderAggregator> doProcess(
            ProcessStack processStack,
            PlaceOrderAggregator aggregator,
            ProcessStepManagerUtil<PlaceOrderAggregator> stepManager
    ) throws RetryableExecutorException, NonRetryableExecutorException {

        if (aggregator.getRealVersionAsString().equals("1.0.0")) { (1)
            String comment = aggregator.getMissingProperties().get("comment").toString(); (2)
            System.out.println("comment = " + comment);

            for (PlaceOrderAggregator.ItemDetail itemDetail : aggregator.getItemDetails()) { (3)
                String note = itemDetail.getMissingProperties().get("note").toString(); (3)
                System.out.println("note = " + note);
            }
        }
        ...

        return stepManager.next(UpdateStockExecutor.class);
    }

    @Override
    public void doRevert(
            ProcessStack processStack,
            NonRetryableExecutorException e,
            PlaceOrderAggregator aggregator,
            RevertHintStore revertHintStore
    ) throws RetryableExecutorException {
        ...
    }
}

You already know that you have to use the same aggregator as well as the same executors for invoking the old transactions as well. Although the missing properties should not be need for the new version(1.0.1), If the event is an old transaction from the version of 1.0.0, the missing properties can be required. Therefore, it is necessary to identify the exact version of the execution (Event). To identify the exact version of the current execution (Event), The framework provides the data version data along with the Room Aggregator Object By default.

1 Check the current execution is 1.0.0 or another version by using the version data that provides by the Aggregator.
2 If the version is 1.0.0, you can get the missing properties from the aggregator object by calling getMissingProperties() method. That pert is based on the root aggregator object.
3 If the version is 1.0.0, you can get the missing properties from the itemDetail object by calling getMissingProperties() method. That pert is based on the root ItemDetail object.
It is possible to get the missing properties and the version of the current execution (Event) in every executor like Command-Executor, Query-Executor and Revert-Executor.