24

My question is similar to this one, but a bit more specific to one case.

Let's say I have a payments model, and this is used by my reporting service, as well as my accounting service.

Is it inescapable that these two services are now coupled by their mutual dependency on the model?

1
  • check RPC (remote procedure calls), maybe it could help.
    – Black Hole
    Commented Apr 25, 2019 at 15:39

5 Answers 5

4

As Ewan mentioned, the two services are coupled by the data exchanged, not the model (from a strictly object oriented perspective). NOTE: I get a 404 when I follow your link, it looks like a private project.

From a web service perspective data is exchanged in a known schema. It could be XML or JSON, but that is less important than the structure of the data and the content inside of it. That allows for a heterogeneous deployment environment.

The service itself can be defined in Go, without an object model at all. You can still consume it with a client built in Python that does bind the output to an object model.

Sharing model objects between client and server in a web service environment is only a matter of convenience for the developer. You don't always have that option, particularly if you are consuming a 3rd party API.


In your specific example, the fact that reporting and accounting services share the same schema for payments doesn't necessarily couple them. Many times you can have a core schema and then have extensions for a particular purpose. The main integration happens in the core schema.

Reporting Service can have fields in addition to the base payments schema that are only germane to the reporting service. Same is true for the accounting service. Yet, as long as the core payments schema is respected any additional fields not used by that service will simply be ignored.

6
  • 1
    Hi Berin, thank you for your answer. I guess what I'm asking then, is where would the core payments schema be defined? Commented Apr 25, 2019 at 7:59
  • A schema is just a definition of fields and types. That's all just a definition that you have to create and use. You can use JSON Schema or XML Schema depending on your transfer medium, but you keep that where you keep your other design documents/etc. Commented Apr 25, 2019 at 12:26
  • 3
    Right, I get that, but if I have a C# class definition, and that class is used by multiple services, where would that get defined? Commented Apr 25, 2019 at 13:12
  • 4
    @GeorgeGrainger I think Berin is interpreting your comment differently from how it was meant, even though his answer adresses your issue. My guess is that you mean: If you use Payments in microservice A and B, do you put the *.cs file in A or B? And the answer would be "both". Microservice A and B will each have a Payments class without a real difference between them. The difference is just how it is used; in microservice A it defines the output format (and it doesn't care about how it is read), in microservice B it defines the input format (and it doesn't care how it was written).
    – R. Schmitz
    Commented May 1, 2019 at 9:34
  • 1
    @R.Schmitz, that's exactly it. Thank you, very helpful! Commented May 1, 2019 at 13:02
12

The other answers are correct as far as the coupling being at the data interface level. But they don't really answer the question posed in the title:

How do I manage shared models among many microservices?

Conventional microservice wisdom is to err on the side of coupling loosely; share as little code as possible.

A brief overview of the problems with sharing code directly can be found here. This excerpt hits on a few good points (emphasis mine):

David emphasizes that all code sharing will attach your services together via the shared code. Creating a single source of truth, adhering to the DRY principle within a single service will create internal coupling but causes no problem in a service with a single responsibility. In contrast, when crossing a boundary, even though some things look the same, they are in a different context and must be different, implemented by different code and using a different data store. David urges that no matter how similar things look, we must resist attaching them because that means we are coupling across boundaries and across different contexts, a direct path to a big ball of mud.

This ebook further illustrates why sharing code across microservices is generally bad, but through the lens of it being a necessary evil sometimes. It is sometimes pragmatic to share code carefully across services. I would recommend reading the "I Was Taught to Share" AntiPattern section in full for a better understanding of how/when to share code across microservices. For posterity, here are the approaches it discusses for sharing code in such cases where it is appropriate:

  1. Shared Project: Using a shared project forms a compile-time binding between common source code that is located in a shared project and each service project. While this makes it easy to change and develop software, it is my least favorite sharing technique because it causes potential issues and surprises during runtime, making applications less robust. The main issue with the shared project technique is that of communication and control—it is difficult to know what shared modules changed and why, and also hard to control whether you want that particular change or not. Imagine being ready to release your microservice just to find out someone made a breaking change to a shared module, requiring you to change and retest your code prior to deployment.

  2. Shared Library: A better approach if you have to share code is to use a shared library (e.g., .NET assembly or JAR file). This approach makes development more difficult because for each change made to a module in a shared library, the developer must first create the library, then restart the service, and then retest. However, the advantage of the shared library technique is that libraries can be versioned, providing better control over the deployment and runtime behavior of a service. If a change is made to a shared library and versioned, the service owner can make decisions about when to incorporate that change.

  3. Replicated Code: A third technique that is common in a microservices architecture is to violate the don't-repeat-yourself (DRY) principle and replicate the shared module across all services needing that particular functionality. While the replication technique may seem risky, it avoids dependency sharing and preserves the bounded context of a service. Problems arise with this technique when the replicated module needs to be changed, particularly for a defect. In this case all services need to change. Therefore, this technique is only really useful for very stable shared modules that have little or no change.

  4. Service Consolidation: A fourth technique that is sometimes possible is to use service consolidation. Let's say two or three services are all sharing some common code, and those common modules frequently change. Since all of the services must be tested and deployed with the common module change anyway, you might as well just consolidate the functionality into a single service, thereby removing the dependent library.

One word of advice regarding shared libraries—avoid combining all of your shared code into a single shared library like common.jar. Using a common library makes it difficult to know whether you need to incorporate the shared code and when. A better technique is to separate your shared libraries into ones that have context. For example, create context-based libraries like security.jar, persistence.jar, dateutils.jar, and so on. This separates code that doesn’t change often from code that changes frequently, making it easier to determine whether or not to incorporate the change right away and what the context of the change was.

Also, related: https://softwareengineering.stackexchange.com/a/366237/209046


Anecdotally, I've done both #2 and #3 before on different projects; by sharing code in a "microservices common" library (explicitly identified as an antipattern above), and by copy/pasting models from one service to another (though these weren't really "microservices") to be maintained separately. The most obvious benefit of #3 was that framework upgrades for each service could be done independently, with the drawback of having to maintain model code in two places (which wasn't so painful with only two services). Regarding #2, I distinctly remember that managing our "microservices common" package was the most painful part of development on that project.

2

They are not coupled by the model.

They are coupled by the format of the data they exchange, which they deserialise to the model.

If they have to talk they have to understand each other. so yes its practically inescapable. Although in theory maybe you could write an AI which could interpret varying structures I doubt it would be easier than simply exchanging the format.

1

I suggest you create a (git) repo that stores the schema, defined in Avro. This way, the schema/contract is well defined and any developer could derive their language specific representation of the contract.

https://avro.apache.org/

-2

I know this is old. But why not think about it like there is no shared model. Each service defines it's own model, and publishes to the world (via it's api) what is required to communicate with it. There is no reason that service A and B need to have the exact same representation of some model concept, in fact each may be concerned with different aspects of any given domain concept, and thus have different internal representations of it.

Not the answer you're looking for? Browse other questions tagged or ask your own question.