This sounds like it could work, but then you start to wonder about how you'd get common code into those 12 services. Our answer at the time was nugets, but we operate in a sensitive domain with proprietary code, so public nuget services were a no go. So, we stood up our own goddamn nuget server just so we could distribute our own stuff to ourselves in the most complicated way possible.
Even if you are using all the same language and patterns everywhere, it still will not spare you from all of the accidental complexity that otherwise becomes essential if you break the solution into arbitrary piles.
One is much simpler than the other. Artifact management is surprisingly complex, it only looks like it works great while you aren't managing many versions and acting more like a monolith, just spread across repos/deploy points.
You already have that pipeline for release/deployment though, don't you? (And it's already hooked up to your SSO or what have you).
> if you have three projects in three checkouts that all have to be coordinated and merged in order so that you don't accidentally break your environment and worry about security and and and....
It's the same as any other library dependency though, which is a completely normal thing to deal with. The cost of separating your pieces enough that you can use different versions of a library in different services is that you can use different versions of a library in different services.
I'm skeptical about microservices as a deployment model, but I'm absolutely convinced that code-level modularisation and independent release cycles for things that deploy independently are worthwhile, at least if your language ecosystem has decent dependency management.
Nuget does provide mechanisms for obsoleting packages, so it's reasonable to enforce that new packages should allow for a few versions worth of backwards compatibility before deprecation and finally pulling the plug.
One of my problems with microservices isn't really the services themselves, but the insane amount of tooling that creeps in: GRPC, Kafka, custom JSON APIs, protobufs, etc. etc. and a lot of them exist in some form to communicate between services.
I'm gonna have to disagree with this. Often when languages add features to their core, it allows for a very different (and often better) approach to writing code. Think Lisp with first class functions, or Rust with the borrow checker. Why should concurrency be any different? This feels like a Blub moment and I would highly recommend you give erlang or elixir a try, just to see what it is about.
If your typical microservice setup is simply distributing your call stack over a network (and oftentimes that's all it is), then you might as well use a language designed to operate in such a manner and reap the benefits of it. That kind of microservice architecture only really exists as a function of the organisation's structure such that teams can work more autonomously.
Been free in my IDE since I compiled the code...
"E2E integration tests"
I can mostly run these on my machine, no need to stand up a cluster to get it running (bonus points if you virt multiple machines on one and still don't need the cluster for complex distributed scenarios).
"you will need some other kind of persistent message queue"
var queue = new Queue(); // then sometime later... queue.Save(); // or queue.EmplaceAndSave(); ... queue.Pop();
Trivial and didn't need a server to set it up.
Lots of tech stack stuff simply disappears without server boundaries and the like to get in the way. There is other tech you have to deal with, but at the small scales it mostly doesn't apply. Which, you as a dev are usually only working at small, testing scales, so you don't usually need to support it.
Yeah, no... you'll lose data for sure, you'll have race conditions or no cooperative queuing between multiple instances of the application. This is exactly the kind of half-assery that people resort to when they say monoliths are so much less complex than microservices. A good monolith still requires all the same hard decisions about modularity, high-availability etc.
Default queue implementations come with co-operating queuing out of the box. I suppose you could use some hand-rolled queue with no internal locking, but this is disingenuous.
> you'll have race conditions or no cooperative queuing between multiple instances of the application.
You're thinking micro-service again here. There's no need to have multiple "instances" at all. Any concurrency is handled internal to the application itself with use of e.g. fibers, threads, etc. A good argument to why monoliths have drawbacks can't be "well its not a microservice". :)
> A good monolith still requires all the same hard decisions about modularity, high-availability etc.
Never said it didn't. What it doesn't require is all the tech stack to support that over many instances, because there's one instance. A large majority of µServices is just re-implementing what your runtime gives you for free.
Too often devs weaponise micro services and say it’s one of the key reasons for microservices is to allow free language choice !
Microservices doesn't need to mean it's the wild west and every team can act without considering the larger org. There can and should be rules to keep a certain level of consistency across teams.
Functional decomp is left to different teams. But the libraries for logging, a&a, various utilities etc are common.
No microservices that don’t meet the stack unless they’re already developed/open source - eg open telemetry collectors.
Edit: I think the article is a path to a book written by the author. It’s more of an advertisement than an actual assessment. At least that’s my take on it.
This works well if you have knowledge redundancy in your organization, i.e., multiple teams that are experienced in each programming language. This way, if one or more developers experienced in language 'A' quit, you can easily replace them by rearranging developers from other teams.
In small companies, this flexibility of allowing multiple languages can result in a situation in which developers moving to other jobs or companies will leave a significant gap that can only be filled with recruiting (then onboarding), which takes much more time and will significantly impact the product development plan.
More often than not, the choice between Microservices and Monoliths is more of a business decision than a technical one to make.
I think that, technically you can use one or the other and make it work. However management is very different in the two cases, so I completely agree with you. I hadn't thought of the part about moving people between teams.
It's my first job but I understand why they chose microservices : 6 teams working on 6 "features/apps" can be managed (almost) fully independently of each other if you split your code base.
It takes effort to keep a monolithic application set up this way, but IMHO the effort is far less than moving to and maintaining microservices. I think a problem is that there's very popular ecosystems that don't have particularly good tooling around this approach, Python being a major example--it can be done, but it's not smooth.
To me the time when you pull the trigger on a different service+codebase should not be code complexity, because that can be best managed in one repo. It is when you need a different platform in your ecosystem (say your API is in Java and you need Python services so they can use X Y or Z packages as dependencies), or when you have enough people involved that multiple teams benefit from owning their own soup-to-nuts code-to-deployment ecosystem, or when, to your point, you have a chunk of code that the team doesn't or can't own/maintain and wants to slowly branch functionality away from.
Could you elaborate on this? I see how "separating data from logic" is a problem but what about the other two?
If you want to enforce a clean interface between an in-process client and server (i.e. a piece of code calling a library interface), then the best model is to think of it as a message passing system, where once a payload is passed from the client to the server and vice versa, the other side should not be able to witness changes to the payload. An immutable payload in this context is the same as the json that goes over the wire between a client and server.
If you really wanted an in-process application to look like a microservice you could take the added step of forcing a serialization / deserialization step on both client and server. I've seen frameworks that do this. But I think immutability, if it can be enforced, is a practical way of solving this problem and far less complex.
Inheritance is a hazier point I was making, in hindsight, because you could use inheritance for data modeling, which is quite fine in some situations... so I think it's effectively subsumed under the "separating data from logic" argument: which is that if you're passing behavioral inheritance from a "server" to a "client", then it gets harder and harder to predict how that client is going to use it and thus it's harder to reason about the functional boundary between two pieces of code. But it's a hazy point because I think the larger and more important point to make, as you point out, is that you simply shouldn't (in most cases) pass any kind of behavior between client and server--just data.
Another approach to look at (besides taking inspiration from Smalltalk) is the actor model, where it's incredibly clear that any communication between modules in an ecosystem is immutable messages. In fact a side effect of the actor model is that it gets pretty easy to move things from in-process to cross-process because most activity in the system is already performed through message passing, so you can just start channeling messages from Actor A to Actor B through a serialization/networking layer rather than in-process if you want to deploy them separately for reasons of convenience or computational needs.
"which is that if you're passing behavioral inheritance from a "server" to a "client", then it gets harder and harder to predict how that client is going to use it"
You mean a "server" returning the base class object whose methods are overridden by a successor class known only to the "server"?
So then, not Rails apps
Though I'm seeing a lot of convergence between environments over time, which makes me think we're all headed towards a nicer future.
For example in Scala, which does a lot of type inferencing, it's typical to tell the linter to require that public methods have explicit types, even though the compiler will reify them at compile time anyhow, because that makes the interface much more robust to refactoring. Meanwhile in a more dynamic environment, in Python it's getting more typical to use type annotations, and, similarly, to especially use them on functions that define a reusable interface.
I figure that the ideal languages in the future have module-level systems where they can define strict interfaces across modules, but then get as crazy and dynamic as they want inside of the modules.
Say you have Libraries A, B, and C, where B and C depend on A and not one another. You can have B call into C via a service, just as you would in a microservice. Now B and C can be versioned and deployed independency. You can also deploy updates to Library A incrementally, tying it to the B and C deployments.
git revert <bad commit>
What I mean is, if you have v2 of your software that introduces a bugfix for module A that's perfectly fine and a bugfix for module B that breaks everything, you can just roll back just module B directly with your deployment pipeline.
There's no need to go back to the code base and make a new release.
I suppose in the end it comes down to the classic issue of needing to pay down of tech debt, which is true in any software ecosystem.
Most substantial rewrites crosses module boundaries. In micro services changing the module boundary is harder than in a monolith, since it can be done in a single commit/deploy.
That being said, I agree that maintaining legacy systems is far from fun.
Unless you have a strong test suite which tests the absence of those bugs. OFC you can never prove the absence of an issue just the continued functionality of your codebase, but re-writes are often prompted by weird "in-between" functionality becoming the norm (or slow/buggy behavior).
Of course a lot of test suites are of dubious quality/many devs have no idea what a good test suite looks like (usually unit tests are some combination of throw-away/waste-of-time, acceptance tests are mostly happy-path and due to recent trends integration tests are all but non-existent).
But in theory, re-writes are fine when you do have a test-suite. Even with a bad one, you learn what areas of the application were never properly tested and have opportunities to write good tests.
The more testing that's been done and the more stable it should be.
The argument for new can be flipped because new doesn't mean better and old doesn't mean hell.
I always find this sentence to be a bit of a laugh. It's so commonly said (by either group of people with a dog in this fight) but seemingly so uncommonly thought of from the other group's perspective.
People that prefer microservices say it's easier to change/rewrite code in a microservice because you have a clearly defined contract for how that service needs to operate and a much smaller codebase for the given service. The monolith crowd claims it's easier to change/rewrite code in a monolith because it's all one big pile of yarn and if you want to change out one strand of it, you just need to know each juncture where that strand weaves into other strands.
Who is right? I sure don't know. Probably monoliths for tenured employees that have studied the codebase under a microscope for the past few years already, and microservices for everyone else.
It's not clear to me that we're ever comparing apples to apples in these discussions. It would seem to me that everyone arguing left a job where they were doing X architecture the wrong way and now they only advocate for Y architecture online and in future shops.
Both have boxes of stuff and the stuff talks to other stuff.
Logistically its a bit easier to scale the one where the boxes are married to closer to the hardware abstraction (microservices on instances) versus the one where boxes are married to the software abstraction (threads with memory), for the same reason one (monolith) is a lot faster (dev/process/latency) (at small scales) than the other (microservice).
You can scale both, really its mostly about fights about the tooling, process, and where your sec-ops decided to screw you over the most (did they lock down your environments or did they make it impossible to debug ports/get logs).
Practically, AWS is expensive, and they're bloated. Cluster environments that let you merge 1000 computers into 1 big supercomputer and have 1 million cores/terabytes of ram, come with different technical challenges that not as many people know how to overcome, or expensive hardware bills.
So I'd say if someone tells you it "has to be" one or the other they are blowing smoke. Micro-services were recently the hip-new-thing so it makes sense some really really bad nonsense has been written in them so people are rediscovering monoliths (and realizing the microservice people were snake-oil salesmen). In 10 years we'll realize again that some monoliths are really badly written and some people without a clue will re-write them as microservices...
Microservices are a bit like that. They make it extremely difficult to insert cross cutting concerns into a code base. Conditioning yourself to think of how to work within these boundaries means you are going to write monolithic applications that are far easier to understand and maintain.
But if you need to change the contract the changes span the service and all of its clients.
No I don't, that's what computers are for. It's why static analysis is good. Instead of knowing what calls what, you say, "yo, static analysis tool, what calls this?".
> The monolith crowd claims it's easier to change/rewrite code in a monolith because it's all one big pile of yarn and if you want to change out one strand of it, you just need to know each juncture where that strand weaves into other strands.
I'm saying, in the monolith situation, with proper static analysis tooling - which even languages like python and ruby have nowadays - you don't "need to know" how all the strands weave into the other strands, you rely on the tooling to know for you.
And in my experience, static analysis tooling for navigating across service boundaries is, at the very least, far less mature, if not just entirely non-existent.
It works surprisingly well. We're evaluating a 3rd language but won't make that choice lightly (if it happens at all.)
We have 14+ micro services, and it's fairly easy to "rewrite the shit pile" when you actually follow the micro designation. One of our services was originally in perl and we, quite mechanically, rewrote it in ruby in a sprint to align with our other services.
Speaking from personal experience, when monoliths and the teams working on them get big enough, you start having "action at a distance" problems, where seemingly benign changes affect completely unrelated flows, often to catastrophic effect.
You make an innocuous looking resource update in the monolith, like an update to a CSS stylesheet that fixes a bug in a flow your team owns, now breaks 10 others flows owned by teams you never heard of because they were using the existing structure for selenium tests or some js that now fails to traverse the dom because some order of selectors changed, etc.
Microservices are as much a team organizational tool as they are a code one. The idea being those that work on the service "know it", and all it does. They can wrap their head around the whole thing. I think some orgs don't really get this point and _start_ with microservices, completely unnecessarily, for the stage they're at as a company. You always start with a monolith and if you get to the point where everyone is stepping on each other's toes from the lack of enforceable boundaries in the code, you do the obvious and start to create those boundaries.
Microservices aren't the only way to do this of course. Any way of dividing up your service with enforceable contracts will work. Modules get designated with codeowners, assigned to various teams. Resources that were once shared get split up to align with the team structures better. Many frameworks allow multiple "apps" or distinct collections of APIs, so you can still ship your one-binary without splitting out the collections into different services. As soon as you have to independently scale one set of APIs but not another, you can state looking at service boundaries again. For the majority, that day will never come.
This is the #1 reason we quit the microservices game. It is simply a complete waste of mental bandwidth to worry about with the kind of tooling we have in 2023 (pure cloud native / infiniscale FaaS), especially when you have a customer base (e.g. banks & financial instutitions) who will rake you over hot coals for every single 3rd party dependency you bring.
We currently operate with one monolithic .NET binary distribution which is around 250 megs (gzipped). Not even the slightest hint of cracks forming. So, if you are sitting there with a 10~100 meg SaaS distribution starting to get nervous about pedantic things like "my exe doesnt fit in L2 anymore", then rest assured - Your monolithic software journey hasn't even begun yet.
God forbid you find yourself with a need to rewrite one of these shitpiles. Wouldn't it be a hell of a lot easier if it was all in one place where each commit is globally consistent?