10

It is said that in CQRS it is easy to fix a bug, you just redeploy and then replay the events.

But, what if one of the events should cause an external system not in your control to "ship an item" to the customer if you just replay the events the item would be shipped twice.

How do you resolve that?

Jas
  • 507
  • 3
  • 13

3 Answers3

6

You need to make a clear separation between events modifying the state of your read model, and events (potentially) modifying the state of external systems. Make sure you do not have any "mixed events" modifying both states together. That way, you can replay your events in a specific "replay mode" where those events for the external system are not fired again. In this mode, you also "simulate" any events which had been originally been initiated by the external system (now you take them from the replay queue instead).

Don't forget, the redeploy step means actually to reset the state of the read model to an earlier point in time. This is probably nothing you can do (or need to do) for the state of the external systems.

Doc Brown
  • 199,015
  • 33
  • 367
  • 565
  • "Don't forget, the redeploy step means actually to reset the state of the read model to an earlier point in time. This is probably nothing you can do (or need to do) for the state of the external systems." --> but what if I want my replay to retry failed external system calls like shipping? in this case my redeploy an replay would not only reset the state of read model but also cause external events, does this sounds fair or i'm missing something? – Jas Jun 10 '17 at 11:27
  • 2
    @Jas: you do not want to abuse "replay" to retry a failed external system call. You use the "replay" for getting your own system's read model in the same state it was before. That means in case of a failed shipping request, your system was informed before about this failure and stored that information somewhere in its state. The replay makes sure this information is still there aftere "redeploy&replay". So after the replay, your system might a apply a "retry shipping in case of failure" strategy (which has nothing to do with CQRS, any robust ordering system just should have such a strategy). – Doc Brown Jun 10 '17 at 23:05
  • Interesting, this is what I had in mind to do, was just wondering if there is a "pattern" on this so I don't reinvent the wheel! – Jas Jun 11 '17 at 08:39
3

From Martin Fowler's event sourcing article:

The fundamental idea of Event Sourcing is that of ensuring every change to the state of an application is captured in an event object, and that these event objects are themselves stored in the sequence they were applied for the same lifetime as the application state itself.

So when you need to restore the state of your system to a certain moment in time you replay the stored state, not the event handlers, up until that moment.

That being said, if you're only working with state data, there shouldn't be any effects on the external system. Unless you have triggers or watchers on your event store in which case you should disable them for the duration of the restore. Since you say that you have no control over the external system there shouldn't be any attempts to restore its state by using the exposed API since you don't know what side effects it may have in their system. If the restore puts the system in an intermediate state (eg. due to failed operations in the external system) this shouldn't fall within the responsibilities of an event replay.

devnull
  • 2,969
  • 20
  • 20
2

But, what if one of the events should cause an external system not in your control to "ship an item" to the customer if you just replay the events the item would be shipped twice.

To choose a specific example, let's consider how an "at least once" approach to side effects might work.

State currentState = State.InitialState
for(Event e : events) {
    currentState = currentState.apply(e)
}
for(SideEffect s : currentState.querySideEffects()) {
    performSideEffect(s)

So the domain model tracks what needs to be done; but leaves the actual doing to the application

In the context of running a command, the basic idea looks the same. The actual side effects happen outside of the transaction that updates the model.

So the unit tests for your model could looks something like

{
    // Given
    State currentState = State.InitialState

    // When
    Events events = List.of(OrderPlaced)

    // Then
    List.of(SendEmail) === currentState.applyAll(events).querySideEffects()
}

{
    // Given
    State currentState = State.InitialState

    // When
    Events events = List.of(OrderPlaced, EmailSent)

    // Then
    List.EMPTY === currentState.applyAll(events).querySideEffects()
}

The main points here being

  • Updating the model is side effect free; the actual side effects happen outside of the transaction that updates the model.
  • An event describing the outcome of the side effect needs to come back.
VoiceOfUnreason
  • 32,131
  • 2
  • 42
  • 79