I get it - you previously worked out a clever solution in a software app you were working on recently and now you find you have the perfect use for it in a different application. Awesome. So you set up some system to share that great code - Git subtree, fancy shell script, whatever it takes to reuse that code, keep things in sync and stay nice and DRY. Great, right?
Maybe not so much. While you may think it's more efficient to reuse code, you've now tightly coupled your apps, making them dependent on each other. If that code has some domain logic or data schema-awareness, you've likely forced a situation where deploying those apps can't be done independently without risk ("independent deployability" being a major benefit of microservices). You could perhaps add some conditionals to make sure it works one way in app B and another in app A - but you can see where that goes, and it's not pretty. Copy-paste would be a better option.
Shared Library Approach
"Ok, ok, fine, I'll break the code out into a separate library that each app can use," you think. This is a bit better but also has its problems. Frequently the code is opinionated to its original purpose and only by copy-pasting it into different apps a few times can you truly isolate the shared functions and see the commonalities in real-use behaviors. And you still run the risk of having to include excess code and spend time creating abstraction layers.
Libraries developed like this can rarely be used as black boxes, so you need to make sure you have proper documentation, helpful comments, and code that is easy to understand. This seems manageable at first if it's just you or a single small team. You're making assumptions, however, that whoever is going to maintain and update this code down the road will understand its purpose in your universe as a shared library and not simply add some feature for the application they happen to be working on at the time, without fully realizing the side effects it might have elsewhere. So now you need to add more tests, both for the shared library as well as the consumer apps to make sure any updates really don't break anything.
Once you have all that sorted, each app can now choose when to upgrade - assuming you follow strict semver practices (you do, don't you?). Even if you do, your library is likely to have it's own dependencies so you need to make sure any service using it satisfies those as well, potentially reducing the ability for the consumer app to evolve. (And god forbid one of those dependencies is another in-house shared library.)
This isn't to say that you shouldn't use any shared libraries - established open-source components are a staple of modern software development. It also doesn't mean you couldn't use in-house libraries that fall under a pure utility classification: ones that aren't application-specific and don't have dependencies of their own. Ones where updates are likely to be additive and are backward-compatible. (It also doesn't mean you shouldn't include UI libraries that, by design, are meant to insure consistency in the presentation layer.)
Code duplication does have some obvious downsides. But I think those downsides are better than the downsides of using shared code that ends up coupling services. If using shared libraries, be careful to monitor their use, and if you are unsure on whether or not they are a good idea, I'd strongly suggest you lean towards code duplication between services instead. - Sam Newman, author of Building Microservices (O'Reilly, 2015).
Reuse as an Anti-pattern
I realize there are various tools to handle dependency management for shared code while using things like mono-repos, etc., but ultimately what I'm suggesting is this becomes an anti-pattern that prevents you from seeing more elegant ways to solve the problem, and creates a brittle monolith-like structure that can be very difficult to deploy reliably.
Often by taking a step back and evaluating the ecosystem as a whole, you can better identify the causes for why you ended up needing the same code in different places and instead design services that eliminate the need for the shared library. Figure out how each service can do its job in a more specific way. This may mean a different data schema or even (yes) denormalization. You may find your application is based on an outdated view of how services should be sliced. Admittedly, creating more services can cause a different kind of complexity and more things to manage, so it does require a certain level of buy-in to a microservices philosophy and its benefits.
Don’t Share Domain Models
Services that require awareness of other services’ implementation details or domain logic leads to them being developed in lock-step, and can be a reason that sharing code seems to make sense. Creating autonomous services that are isolated from each others’ inner workings will make them more resilient and allow for more organic growth.
An example I recently came across in one of our apps was that a particular function had a condition that included specific user-types to determine who should be allowed to use a certain filter. A similar function was also used in our authorization service — so one approach would be to use a shared utility to make sure the functionality is consistent in whatever app uses that filter. This would mean, however, that the authentication service schema (user-types) would need to be kept in sync across multiple services.
We solved this problem by making sure the filter attribute was only provided if it was applicable in the first place. This removed the need to have entitlement specifics in the downstream application and simplified the logic. Requests are more purely data-driven and decisions binary. And when we did add a new user type, we only needed to update one service.
Embrace Redundancy (within reason)
Having been involved with various software projects that were actual monoliths as well as microservice architectures that ended up as distributed monoliths, I can say I’m firmly in the microservices-done-right camp. The definition of what’s “right” certainly is situational, but in the end you want teams to be able to work on and deploy applications independently — even if that means some redundancy.
Top comments (1)
I think this is all reasonable, and also controversial in its own way.