0

I’m refactoring a microservice project where multiple services share the same domain objects. Over time, these objects have diverged across services, causing inconsistencies.

To solve this, I plan to move them into a shared dependency (library) as a single source of truth. Currently, some domain objects are annotated with JPA (@Entity, @Id, etc.), but I think the library should remain framework-agnostic and only contain pure domain objects. My concern is that this could lead to a lot of mapping code between domain objects and persisted entities.

Also, any change to a domain object in the library will impact all microservices using it, which may happen frequently.

My questions is:

  • How to reduce the impact of domain object changes on microservices?
8
  • Have you researched the DDD concept of a Shared Kernel, along with the benefits and risks of implementing one? Commented Sep 12 at 15:42
  • Please edit the question to limit it to a specific problem with enough detail to identify an adequate answer. Commented Sep 12 at 16:42
  • 2
    The fact that these concepts diverged over time is a strong sign that they shouldn’t be unified in a library. One of the key concepts in DDD are Bounded Contexts: concepts with the same name can have different behavior in different contexts. Creating a shared library for these things that look similar, but have different nuances, can cause a world of pain. Commented Sep 13 at 10:44
  • 1
    On the other hand, if these concepts are really the same thing, the fact that they exist multiple times is an indication that the boundaries are wrong. Also in this case a library is probably not the best solution. Instead some services could be combined to form a single service that matches the bounded context. Commented Sep 13 at 10:52
  • 1
    "These concepts are meant to remain consistent" - RikD has a point. We don't know enough about your domain, so keeping them consistent might be the right choice, but one of the lessons of DDD is to re-examine initial assumptions, such as that one. If each set of clients pulls the model in its own direction, you might end up battling an exponential rise in complexity trying to make a single model that serves everyone. In that case, it might be better/simpler to deliberately break it down into several models with a small but explicit area of overlap - this is what Bounded Contexts are all about. Commented Sep 14 at 18:02

2 Answers 2

3

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.
4
  • Thanks for your answer and the example — it really helps me understand the architectural mindset. However, based on your User example, how would you handle a core business change that affects the user across all domains (e.g., during login, purchase, or delivery tracking)? For instance, let’s say we now need to introduce a new property that is relevant in each of these domain-specific contexts. Would you simply update every domain concept in each microservice accordingly, and that’s considered the right approach? Commented Sep 15 at 8:43
  • 1
    @Ryley38, yup. That's what you would do. A DRY codebase comes with tradeoffs: coupling. The coupling makes sense when you use the same name in each service, but giving those things a domain-specific name allows you to realize that maybe these things shouldn't be coupled, and that's ok. Commented Sep 15 at 10:43
  • Thanks. I’ll mark your answer as the solution since it made me reconsider how I should approach this architecture, and it reminded me that DRY is not always the only answer. Commented Sep 15 at 21:48
  • Always appreciative of the green checkmark. Glad I could help! Commented Sep 16 at 0:18
2

Some of the core advantages of microservices are that they are independent. This usually includes independent deployment and technology independence.

If your goal is to have a shared library, you need to be careful to keep these advantages. Using the same language or framework across all services may be an acceptable compromise, even if it does reduce the technology independence.

But keeping a "single source of truth" while allowing for independent deployment seems difficult. For independent deployment to work, you cannot require that all services use the same version of the library at any one time. So you need to be extraordinarily careful when designing the library to maintain backward and forward compatibility. This might be manageable if it is some foundational library that is changed infrequently. But if you are using terms like "domain objects" and "frequent changes" to describe it, I'm guessing backwards compatibility will be difficult.

One approach is that each microservice provides its own API that includes any domain objects. This API should be versioned and include some kind of support guarantees, so that any other service can continue using the old API some time after a new major version is released. Ensuring backward compatibility will still be difficult, but this should help maintain the independence of services.

Keep in mind that the microservices aim to solve the problem of coordinating a large number of teams, where the cost of all this extra testing and version handling is less than that of dealing with a large number of developers in a single code base. If you have a smaller number of developers, it might just be easier to release everything at once. This might indicate that you really have a "distributed monolith", but if that is the case, it is probably useful to recognize it so you can deal with it appropriately.

5
  • I understand the core advantages of microservices, but I’m struggling to see how this architecture handles domain objects that are shared across multiple services. For example, suppose there is a main object that serves as the central point of your overall project and is used by many different services. How does a microservices architecture handle changes to a property in such a shared object? Do you update every single microservices? Or is it just a code smell showing the project is badly designed? Commented Sep 12 at 20:22
  • Note that wanting to share some logic/models across microservices is not uncommon, though I would generally not refer to them as domain models (but that's just semantics). For example, the sender and receiver of a service bus would benefit from having a single source of truth on the message structure. Or there might be some ecosystem wide "domain" concepts that you would like to unify, e.g. a custom built logging framework that you wish to standardize across your ecosystem. [..] Commented Sep 13 at 0:00
  • [..] In these scenarios, you can keep independent deployments even with these distributed packages, but it requires careful versioning to avoid breaking changes (semver is a huge help here). Commented Sep 13 at 0:01
  • 1
    @Ryley38 The field of software development, in practice, is divided between those who individually re-model things across services and those who rely on packages. Both come with their own challenges. Neither is wrong (when following accepted practices) but you have to judge this based on your situation and which benefits/drawbacks better align with your current scenario. Commented Sep 13 at 0:04
  • @Ryley38 I made some updates to the answer. But if you have a "main object that serves as the central point" it may indicate you have to many dependencies between services. One of the greatest difficulties with microservices is identifying the boundaries between services, and doing this well require high familiarity with the domain. Commented Sep 15 at 6:46

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.