“Closing the books” pattern in Axon 5

Question about implementing “closing the books” pattern in Axon 5

I have a command handling component named ValidateSupportRequestCommandHandler that needs an event-sourced entity. This entity is populated by the following events:

  • GiftRegistrated
  • SupportApproved
  • WaitingSupportApproved

With these events, you can determine the current balance. Based on this balance, either a SupportApproved event, a tuple of (SupportApproved, WaitingSupportApproved), or no events are added to the event store.

The problem is that the number of events that need to be sourced could potentially become huge. I want to implement the “closing the books” principle by introducing an EndOfTheDayBalance event. This way, only the most recent EndOfTheDayBalance and the relevant events that occurred after it need to be sourced.

What is the best way to implement this in Axon 5? More concrete, Is there a way to define an EventCriteriaBuilder that filter events of a specific type occured after a specific event?

Hi! @Remco_Ruijsenaars
yes, thanks to proper Tags usage, you can define the criteria where:

only the most recent EndOfTheDayBalance and the relevant events that occurred after it need to be sourced

In order to achieve that please tag your events with a specific day (let’s assume it’s integer for easier example) and the day needs to be the @TargetEntityId (or part of the id if you use custom object or you want to filter by more tags) to be able to use the value in criteria builder.

Then you can do something like below:

        @EventCriteriaBuilder
        private static EventCriteria resolveCriteria(int currentDay) {
            return EventCriteria.either(
                    EventCriteria
                            .havingTags(Tag.of("day", String.valueOf(currentDay - 1)))
                            .andBeingOneOfTypes(EndOfTheDayBalance.class.getName()),
                    EventCriteria
                            .havingTags(Tag.of("day", currentDay))
                            .andBeingOfAnyType()
            );
        }

The example with such custom criteria and id is in the Getting Started guide:
https://docs.axoniq.io/axon-framework-5-getting-started/implement-command-subscribe-student/#_event_criteria

Please tell me if it works for you!

Hi @MateuszNowak,

Thank you for your quick reply.

I implemented the EventCriteriaBuilder the way you suggested. For the day tag, I used a java.time.LocalDate. Check out the following code snippets:

@EventCriteriaBuilder
private static EventCriteria resolveCriteria(ValidateSupportId validateSupportId) {
    var supportDate = validateSupportId.supportDate();
    var previousDay = supportDate.minus(1, ChronoUnit.DAYS).toString();
    var fundingId = validateSupportId.fundingId().toString();
    return EventCriteria.either(
            EventCriteria
                    .havingTags(Tag.of(DomainTags.DAY, previousDay), Tag.of(DomainTags.FUNDING_ID, fundingId))
                    .andBeingOneOfTypes(
                            EndOfTheDayBalance.class.getName()
                    ),
            EventCriteria
                    .havingTags(Tag.of(DomainTags.SUPPORT_DATE, supportDate.toString()), Tag.of(DomainTags.FUNDING_ID, fundingId))
                    .andBeingOneOfTypes(
                            GiftRegistrated.class.getName(),
                            SupportApproved.class.getName(),
                            WaitingSupportApproved.class.getName()
                    )
    );
}
record ValidateSupportId(FundingId fundingId, LocalDate supportDate) {
}
public record ValidateSupportRequest(UUID requestId,
                                     FundingId fundingId,
                                     BigDecimal amount,
                                     MemberId giver,
                                     MemberId receiver,
                                     LocalDateTime requestDate,
                                     LocalDateTime supportDate,
                                     LocalDate day) {
    @TargetEntityId
    private ValidateSupportId validateSupportId() {
        return new ValidateSupportId(fundingId, day);
    }
}

I was wondering if there is a problem with the following scenario:

The EndOfTheDayBalance is appended to the event store at 23:59:58 by a scheduled processor. However, one second later at 23:59:59, a new SupportApproved event is appended to the event store via the normal flow. This last event won’t be sourced the next day, will it?

Looking at your code — yes, you’re correct. The last event won’t be sourced because of the criteria: it only loads EndOfTheDayBalance from the previous day.

The question is whether we should allow appending SupportApproved after EndOfTheDayBalance. If the “books are closed” — which is exactly what this pattern represents — then no events should be appended to a closed period.
I think your business logic should reject such cases.

One more consideration: Using “days” might be tricky if your app operates across different timezones (I don’t know your specific setup). You might prefer using a “support period” concept instead — an abstract ID that external systems coordinate around, defining the exact start/end of each period.

I’m preparing a sample Axon 5 application for educational purposes.
Thank you for these new insight, they will help me explain this concept more clearly to my students.