Evolving messages contracts

Component: NServiceBus

In message-based systems the messages are part of the contract, which defines how services communicate with each other.

Evolving contracts over time is not an easy task and the appropriate strategy should be defined individually for each system. Generally that problem can't be resolved on the infrastructure level, therefore NServiceBus users need to analyze their systems, consider how they expect them to evolve and define the strategy which will make the most sense in their particular circumstances.

This article presents basic guidelines for choosing contracts evolution strategy, avoiding common mistakes and ensuring the contracts will be easy to evolve over time.

Designing contracts

Messages

Ensure that messages can be evolved by following the general messages design guidelines.

Solution structure

Messages define the data contract between two endpoints.

It's recommended to use a dedicated assembly for messages definitions. By keeping messages in a separate assembly, the amount of information and dependencies shared between services is minimized. Messages definitions can be divided between multiple assemblies, which might be useful in more complex systems, e.g. to narrow down the number of contracts exposed to different services.

It's also possible to share messages as C# source files, without packaging them into an assembly. The main advantage of this approach is that messages don't need to be compiled against specific NServiceBus versions, so it's never necessary to use assembly redirects.

Common challenges

Default values

When a message is extended with additional properties it's necessary to carefully examine what will be the default values for those properties, especially if endpoints running in other versions don't recognize them. In particular, it's important to consider how clients might interpret the default value and provide appropriate guidelines for them.

Handling breaking changes in contracts

There is a number of approaches when it comes to handling breaking changes in messages. One of the simplest is adding a version to the type name and creating handlers for the new contract. The handlers for the old contract might still be running, to handle in-flight messages and messages generated by old endpoints.

When there are significant changes in a message type, such as adding or removing property, changing the property type, etc. the upgrade process should consist of the following steps:

  • Update contract to the new version.
  • Update senders to use the new contract version. Ensure changes are visible for receivers, such as:
    • Decorate the existing property with Obsolete attribute with a warning when removing or renaming properties.
  • Update receivers to handle the new contract version. Make sure the new properties are handled correctly, e.g. instead of relying on .NET to set the default value for int Age = 1, it's better to use nullable types and represent missing values as null.
  • When all senders and receivers are updated and in-flight messages in the old format have been handled, obsolete the properties and throw an error, or simply remove them.

Another approach for handling breaking changes is to modify serialization formats. The step-by-step guidance is provided in the Transition serialization formats formats.


Last modified