Reducing the impact (or blast radius) of domain model changes indicates that micro services are not sufficiently independent. This tidbit, though: "Over time, these objects have diverged across services, causing inconsistencies," makes me wonder whether these inconsistencies are actually the problem. I would recommend analyzing what those differences are, and the intent driving their introduction.
When properly separated, micro services can have classes with the same name, but represent different purposes within the overall system. I like to question this common name to see how similar these things are. For example, a User will likely exist everywhere. First the user logs in and adds items to their shopping cart. Then they pay and schedule a delivery. Saying a "user" exists in the login, shopping cart, checkout, and warehouse services isn't exactly true to what that means from a business standpoint.
When logging in, you have a User. When shopping, that user is a Customer (who may not be logged in). When paying, that user might still be considered a Customer, but when scheduling a delivery? They might be considered a DeliveryRecipient.
So, in the services you have the concept of a person using the system, and it becomes tempting to put a User in every system; after all, a user has a name, address, and payment info, and this is needed in almost all areas of your application. The difference here is what that name, address, and payment info means from a business perspective.
- When logging in, the person is a User who needs to authenticate; this is a domain-specific term within security.
- When the person is shopping, they are a Customer; this is a domain-specific term in retail.
- When the person is paying for their purchase, they are still a Customer.
- When the person has paid for their products and is awaiting delivery from the warehouse, they are a Delivery Recipient; a domain-specific term in warehousing and shipping.
Changing the name of this entity from User to something domain-specific helps fight the urge to centralize these concepts and share them across micro services. This keeps your services independent, and the differences in names allow us developers to feel more comfortable with the duplication, because it's not really duplication; it's a different representation that happens to contain many of the same data points.
The trouble with centralizing these things is you couple micro services together. As soon as they are coupled, they cannot be deployed independently without breaking things. This is the exact opposite of the main benefit micro services bring.
I suppose this is a bit of a frame challenge which I can summarize as:
- Ensure you aren't dealing with different representations of the same thing across services. Make sure the "inconsistencies" aren't actually masquerading as truly different entities that coincidentally share some data.
- If they really are the same entity, it might be time to consider combining micro services if you need to make the same change for the same reasons to all affected services, especially if you must change a cluster of services to avoid breaking things.
- Accept that a DRY codebase is not a panacea for every engineering problem. You may be copying and pasting some code in between micro services, but you gain a loosely coupled architecture, which keeps your services independently deployable and scalable — the very definition of a micro service.