Teams use microservices and microfrontends to reduce mental load, decrease accidental coupling of unrelated code, to deploy independently, for fast feedback loops and CI runs, to isolate issues or to scale with team size. In a microservice architecture, you should avoid shared dependencies or a common core library. To be more precise, don’t share libraries that increase coupling.
As IT professionals, we know that. However, despite our best intentions, shared dependencies sometimes creep in and cause issues. From what I’ve seen, here are the most frequent occurrences in the backend:
- Common code related to logging, tracing, monitoring. „Hey, what’s wrong with these? Every second article online says, this is fine!“ It depends. Measure how much time you spend on this code and how big it is compared to business logic. Stick to standards and defaults as much as possible. Use sidecar containers and a service mesh for the heavy lifting. Keep code that has to stay in the application itself to a minimum and copy and paste it instead of moving it to a common dependency.
- Common business logic. Look out for
ShippingFeeCalculator. Check whether you cut your bounded contexts correctly. If you expect certain identifiers, let’s say an insurance number, keep the validation where the data initially enters the system. There is no need to check again in every downstream services that it in fact is 9 digits long and ends in a Z. Don’t reinvent the wheel regarding date & time calculations, there are a lot of great libraries out there. If only a few lines of code remain, don’t put them in a library, copy them.
- A dependency that fixes the broken framework you use. This is surprisingly frequent, I have seen code littered with comments saying „This is a workaround for upstream bug 1234…“. Report the issue, create a pull request, become an upstream contributor. Same goes for adding functionality. Your framework only supports XLIFF for translations, but you need .po-file support? This sound like a good addition you could push upstream.
- Common data models or integration tests. Keep the data model simple and small. Only map what a consuming service needs, not the entire structure. Look into consumer-driven contract testing (Pact).
But my buttons! My beautiful buttons!
Some frontend developers, often citing Martin Fowler, believe that UI component libraries are exempt from the “micro” mindset and using shared dependencies for these won’t cause any issues. In truth, most of the same considerations apply as in the backend.
An eager and gifted developer creates a „generic button component“. It consists of just a couple lines of code that is introduced in one place. It is shiny and beautiful and useful and better than what was there before. Sounds good? Not so fast…
The biggest mistake you can make is creating a private npm package “our-company-ui”. This will ensure dependency hell: “… and the shipping information pages are only compatible with our-company-ui version 4.x, which is only compatible with an older React version, but don’t worry we have an upgrade path in mind…”. Soon several special cases are added to the common library, cluttering every component: “… and if the button is used for file uploads, we have these extra properties here…”. It encourages violating YAGNI: “I know we currently don’t use input type ‘tel’ or ‘search’ anywhere, but come one, it’s an input component, it should support it!”, it’s a case of NIH and all the other bad acronyms.
The pizza of doom
What if, instead of “our-company-ui”, every functionality gets their own dependency? That way the micro principles can be adhered to.
our-company-button 1.2.0 can be used together with
our-company-textinput 1.0.0 and
our-company-fileupload 2.0.0. Of course the default border size in version 2 is a bit thicker than in version 1, so while these can be used together, you really shouldn’t… Compatibility between such dependencies can quickly become a complicated chart, so a crafty team might introduce a meta dependency, let’s call it “our-company-ui” again, which just depends on the individual components. Side note: This pattern is not necessarily found in frontend only. There are „our-company-spring-boot-starters“ out there and all other kinds of recipes of doom.
Advocates of this way, especially when embedded in a cookie-cutter approach to micro development, will try to defend it: “This way you can either quickly get started with a known set of compatible components, but if you need to use a newer version in one or two cases you can still easily override it!” This will never happen. If it works, developers will stick with it, the tests are green, it runs, what do you want from me?! The meta dependency will again blow up the container size and deployment times unnecessarily.
Of course it’s valuable to know a common set of shared dependencies that are compatible, especially if you need to rapidly create a lot of services. However this can be done by a skeleton or sample project which is then adapted according to needs. Depending on the size of the project, a tool that generates skeleton code may be a viable option.
UX or UI?
Think about whether you should focus your development efforts on user experience or looks. Usually, it’s the former. It should be as easy as possible to enter your shipping details, there should be great validation and auto-fills and handy maps and everything should be clear and fast. As long as it does not look like garbage, it often isn’t that relevant HOW it looks exactly. Use an existing UI library in this case, instead of reinventing the wheel.
If this is not the case, and looks are important to your application, or if your custom-made existing UI library is just so amazing that you won’t drop it, then open source it. That way the burden of maintaining your UI components is at least split with some outside help, you’ll get valuable feedback and it’s good for reputation.
Feel free to tell me your thoughts in the comments.