Disclaimer
Treat this post more of my personal thought in which I tried to build a case from what might go wrong when developing with micro services. My purpose is not just to share my thoughts but also trying to welcome other opinions that may be different from what I’m going to state here as part of my learning process. So first of all, thank you very much for reading my post and leaving your comments (if any) 🙂
Intro
As part of what we learnt from university or even for people that starts to learn about software development, we were taught to reuse code, and it is one of the reason we build a function. Not just that, terms like “don’t re-invent the wheel” or DRY principle told us that. But the question is are they an absolute thing? I believe that everything must be in context.
Problem
Here is one of the example I’d like to share which “to me” re-use should not be applied.
Consider we have the following situation where service A calls serviceB, and service B calls service C as described in diagram below:
The problem happen if a response model, enum, or object or DTO from C is “re-use” to B then get passed to A - see some snippet below:
interface ResponseC {
prop1: string
prop2: string
}
const endpointC: Promise<ResponseC> = async() => {
...
return { prop1, prop2 }
}
import { ResponseC } from 'path-to-c-or-common'
const endpointB: Promise<ResponseC> = async () => {
...
const { prop1, prop2 } = await serviceC.endpointC()
return { prop1, prop2 }
}
import { ResponseC } from 'path-to-c-or-common'
const endpointA: Promise<Omit<ResponseC, "prop2">> = () => {
...
const { prop1, prop2 } = await serviceB.endpointB()
return { prop1 }
}
So what is the problem?
The above use case of re-use defeats the purpose or an anti-pattern of the idea of micro-service itself.
One of the main ideas behind micro services is separating responsibilities / loosely coupled (you can read it yourselves from https://microservices.io page).
Argument #1
You may argue that having each micro to have their own types will slow down the development because we need to do mapping each time to call other service.
BUT having that "extra" work is what makes our system "decoupled" and will give us more flexibility in future (and the speed for future if I may put it this way).
Think when a change is required to change the shape of the shared model on service C, this will impact not just service B but also service A. Change suddenly becomes hard, because there are a lot of wrong dependencies, and the complexity to do the change will add up the more service using the same shared object. I'm using the word unnecessary dependencies to refer to any "2nd layer" service.
A data model/object should only be consumed to the next layer, which should be mapped to that internal layer data object. If any change in service C, it should only affect service B (note: I'm not talking about fault tolerant here, just in case)
Let's now put in another illustration, if those services above are actually managed by different companies, where A is the client/consumer of B, and B is the client of C - If you are the owner of C, you would need to inform only your clients of any update to your API (in this case B), and not have to think about the client of your client (A).
Argument #2
Hey, but we can use Unions or any other Utilities Types in typescript be it
Omit, Pick, Partial, etc
Now the fact, that you still need to "re-adjust" these types based on the changes on C, you are still coupling your services.
Argument #3
Ok, what about sharing
enums
? Let's say there is a CreditCardType enums on payment-service which I need to reuse on my other service. Isn't it credit card type will be the same anyway?
My suggestion is still not sharing those enums, especially if you have some sort of CreditCardType
field stored in your other service. They may apparently be the same, but there may be use case where your other-service may want to restrict/limit what can be used.
Your service should be agnostic to other service (or has a clear separation of responsibilities with other service), if not probably it should belong to the same micro service
Conclusion
code-reuse if not used in the right context, will slow down the development, increase the coordination effort and hinder progress as it will make micro services to not be independent anymore.
Some Code Wisdom Reference
“Prefer duplication over the wrong abstraction”
There are two "Rules of Three" in reuse: (a) It is three times as difficult to build reusable components as single use components, and (b) a reusable component should be tried out in three different applications before it will be sufficiently general to accept into a reuse library.
"You want to wait until reusability becomes obvious
Note
I will try to keep updating this page with more arguments I encountered. Feel free to post your comment, and keen to learn the other perspectives 🙂
Top comments (0)