Command/Query versioning

Hi all,

Commands and Queries are conceptually part of your API. If you change these you might break the contract with other services.

What’s the general approach to have backwards compatibility when changing commands and queries?

Kind regards
Koen

anybody can help please,

I’m not quite sure what is your question exactly. Commands and Queries are classes that are part of the public API as you correctly stated. In that sense they should follow the same rules you apply when releasing new versions of the API. What exactly those rules are depends very much on your domain specifics.

Generally speaking you have actors on both sides of any API that will be affected by changes. Let’s call them consumers and providers for simplicity. A consumer is the actor that uses the API to interact with the system. There are likely many consumers and/or they may change frequently. A provider is the actor (that can be independently deployable unit) that does something on behalf of the system. There are likely few providers and likely they don’t change often.

So in some cases you may be in a position to demand that every single API consumer and provider upgrade at the same time. In other cases you may force only providers to upgrade (perhaps because there are not many of them and they are easy to control) while still allow consumers to work with the old API. In yet another cases you may have to keep both consumers and providers backward compatible for indefinite time.

As you can see it’s next to impossible to have a “general approach to have backwards compatibility” across different domains and use cases. You should have one strategy for your particular domain model though. Whatever it is, make sure that command and queries follow the same approach.

If I misinterpreted the question and that is not what you are asking, please try to rephrase it or explain in more details what is the actual concern.

Let’s make my question more concrete.

I have an aggregate with a command handler for “ChangeQuantityCommand”.

We want to change this command and add some extra field(s).

What’s the best approach when you apply continuous delivery and you need to keep the old version available next to the new version.

What to do with the command?

  1. What would happen if we still had a old client sending the command with a missing field?

  2. Should we keep two versions of this command in the codebase? “ChangeQuantityCommandV1” and “ChangeQuantityCommandV2”?

Same question applies to queries.

For events, we have the event upcasters. Although you might get into the same situations when the consumers do not know the new version yet… and don’t know how to “downcast” it.

Kind regards
Koen

So let me rephrase my previous comment and adjust it to your particular example.

With a command being the API in question you have

  • command handlers which act as producers
  • command senders which act as consumers

The change you are talking about “add some extra field(s)” is

  • breaking change for all producers. They must know how to handle the new field as new consumers may be using it.
  • non-breaking change for all consumers. They can still use the old version.

With that in mind you have several options:

  1. Update the command keeping the FQCN and update the handler(s) to use the new data. Then you have the following options regarding handling old commands:
    1. Reject the command if data is missing.
    2. Add some logic in the command that generates the missing data.
    3. Make sure all handlers can handle missing data.
  2. Introduce a new command with different FQCN and respective new command handler(s). Keep the old commands and handlers (perhaps extract common logic to avoid duplication). Threat both path the same from domain logic perspective.

Please note that things would be different if you were to remove or rename a field. Then you are actually breaking everyone.

I’m sorry but I can’t answer “What’s the best approach”. Based on what you’ve said so far, the above are conceptually the approaches you can possibly take. Which one makes most sense is something you need to evaluate in the context of the specific application taking into account its overall impact on the system (including any side effects).

Regarding the event upcasters you mentioned, that is essentially a way to implement option 1.2 from the above. Arguably that is the only option that makes sense for processing persistent immutable events. With commands and queries you simply have more options and thus no silver bullet.

I hope that helps even though it does not directly answer your question.

You would want to keep your messages both forward and backward compatible when you are doing rolling upgrades of your system.

What I mean with this, is that you message structure should be compatible for both versions of the application at once. You could achieve this by doing the following:

  • Never change the semantic meaning of a message. If you notice this needs to happen, you have a new message.
  • Never remove fields. This is a breaking change. Instead, deprecate the field you would like to remove, so that it can be removed in the release there after.
  • Never rename fields. This is a breaking change. Instead, deprecate the field you would like to rename, so that it can be removed in the release there after.
  • Only add new fields. Make them optional or provide a sensible default, if possible.
  • Allow the Serializer to ignore unknown fields. That way your old version can deal with the new commands (which have additional fields) just fine.

This is my 2 cents, hope it helps.

2 Likes