Some languages (e.g. Go) make this easier than others by allowing two versions of the same import to coexist, but this is why we broke up our Django monolith a few jobs ago.
IMHO unless you have a strong engineering culture, having the same dependencies is the only way to ensure they are all properly updated (regarding security updates, and replacing deprecated dependencies/versions.)
In my experience doing this at AWS and elsewhere, no, the services case is much easier. Even just the ability to do releases in chunks 1/14 of the size is a huge advantage. On top of that, your services are going to have smaller dependency chains, and transitive dependencies are truly where dependency hell[0] comes into play. It boils down to complexity being multiplicative rather than additive. Depending on the size of everything, each service is probably more like 1/50th the amount of work rather than 1/14th.
> IMHO unless you have a strong engineering culture, having the same dependencies is the only way to ensure they are all properly updated (regarding security updates, and replacing deprecated dependencies/versions.)
This is working under the assumption that all your code and services should always be updated to the latest, which isn't necessarily true. I still have smaller, infrequently updated services running and happily chugging away that run on Python 3.5. Other codebases have been upgraded to Python 3.10, sure, but what's the harm in letting this one run on an end-of-life Python, especially given that it's a backend service, with no public access, that only communicates via RPCs? A vulnerability might be found in Python 3.5 that necessitates an upgrade, but it's not like moving to the latest version of everything prevents that -- i.e. "Heartbreak"[1] only affected OpenSSL 3, not people still using OpenSSL 1.1!
[0] https://en.wikipedia.org/wiki/Dependency_hell
[1] https://arstechnica.com/information-technology/2022/11/opens...
The chance of a percentage of 14 microservices just plain not using that feature is bigger than the chance of that monolith not using it.
A completely contrived and fake example:
The language you're using has a complete refactor of its math library, everything must be rewritten in a new style that's incompatible with the old style.
In a microservice world there's a good chance that very few of those actually need the high-powered math library.
But your monolith has a few sections that use it so the whole shebang is delayed until that bit is rewritten to the new API.
On the flip side, at some point monoliths can get large enough that upgrading libraries or runtimes becomes an incredibly difficult problem. When I was at Amazon a major service was finally getting upgraded from Java 7 to Java 8. Took a cross-functional team 6 months (!) to make it happen due to the massive amounts of existing code. And these are AWS engineers who are much technically stronger than average.
I don't condone the 500 LOC style of true microservices, but people tend to go back to the extreme of "just make everything run in the same process with the same runtime and same dependency chain" as if that doesn't come with its own set of headaches.
(also for what it's worth -- in correctly done SOA, you don't need to upgrade libraries in lockstep. So that Spring upgrade can be done on a per-service basis as needed.)