Thanks Scotty, you’ve really thought about this!
Personally, I’m a fan of “shared design patterns with some copied code” as an approach. We’re able to agree on a set of interoperability standards (ie: Azure Service Bus for messaging) with some hard rules like “don’t remove a message properties”. Removing fields will break an existing consumer but newly added fields can be ignored.
All of our event-based messages have a single “source owner” who is responsible for generating the message. This helps to make sure that our formatting is consistent. We have many workers for each message, but these workers only consume the fields that they care about.
We generally find that the amount of copied code between microservices is very small. There are some shortcuts that we borrow, but we get a lot for free with our vendor supplied packages (ie: MongoDB C# Database Driver) that there’s rarely a need for an extra data layer.
You can’t really avoid the refactoring problem though, but I’d say just “embrace it”. By keeping the services small the refactor isn’t ever a big deal. Our API microservices have a maximum of 10 REST endpoints or 5 message consumers. Plus, we always split up our consumers microservices (workers) and API’s so there’s never too much code.
It takes some discipline, but is definitely worth it.