A champion will rise with a clean architecture and design in microservice form that addresses all high visibility pain points, attributing forecasted benefits to the perceived strengths of microservices. The team buys into the pitch and looks forwards to a happily-ever-after ending.
The reality though is that the team now has multiple problems, which include:
- Addressing conceptual debt that hasn't gone away. - Discovering and migrating what the legacy system got right, which is often not documented and not obvious. - Dealing with the overheads of microservices that were not advertised and not prominent at a proof-of-concept scale. - Ensuring business continuity while this piece of work goes on.
I would propose alternative is to fix your monolith first. If the team can't rewrite their ball of mud as a new monolith, then what are the chances of successfully rewriting and changing architecture?
Once there is a good, well functioning monolith, shift a subset of responsibility that can be delegated to a dedicated team - the key point is to respect Conway's law - and either create a microservice from it or build a new independent monolith service, which aligns more to service oriented architecture than microservices.
But too many are eager to jump into distributed computing without understanding what they are bring into their development workflow and debugging scenarios.
Absolutely. More people need to understand the binding hierarchy:
- "early binding": function A calls function B. A specific implementation is selected at compile+link time.
- "late binding": function A calls function B. The available implementations are linked in at build time, but the specific implementation is selected at runtime.
From this point on, code can select implementations that did not even exist at the time function A was written:
- early binding + dynamic linking: a specific function name is selected at compile+link time, and the runtime linker picks an implementation in a fairly deterministic manner
- late binding + dynamic linking: an implementation is selected at runtime in an extremely flexible way, but still within the same process
- (D)COM/CORBA: an implementation of an object is found .. somewhere. This may be in a different thread, process, or system. The system provides transparent marshalling.
- microservices: a function call involves marshalling an HTTP request to a piece of software potentially written by a different team and hosted in a datacenter somewhere on a different software lifecycle.
At each stage, your ability to predict and control what happens at the time of making a function call goes down. Beyond in-process functions, your ability to get proper backtraces and breakpoint code is impaired.
Monoliths aren’t merely monolithic in terms of having a monolithic set of addressable functionality; they are also monolithic in terms of how they access resources, how they take dependencies, how they are built, tested and deployed, how they employ scarce hardware, and how they crash.
Microservices help solve problems that linkers fundamentally struggle with. Things like different parts of code wanting to use different versions of a dependency. Things like different parts of the codebase wanting to use different linking strategies.
Adding in network hops is a cost, true. But monoliths have costs too: resource contention; build and deploy times; version locking
Also, not all monolithic architectures are equal. If you’re talking about a monolithic web app with a big RDBMS behind it, that is likely going to have very different problems than a monolithic job-processing app with a big queue-based backend.
The other advantage of the network boundary is that you can use different languages / technologies for each of your modules / services.
Let's say we have 3 different modules, all domains: sales, work, and materials. A customer places an order, someone on the factory floor needs to process it, and they need materials to do it. Materials know what work they are for, and work knows what order it's for (there's probably a better way to do this. This is just an example).
On the frontend, users want to see all the materials for a specific order. You could have a single query in the materials module that joins tables across domains. Is that ok? I guess in this instance the materials module wouldn't be importing from other modules. It does have to know about sales though.
Here's another contrived example. We have a certain material and want to know all the orders that used this material. Since we want orders, it makes sense to me to add this in the sales module. Again, you can perform joins to get the answer, and again this doesn't necessarily involve importing from other modules. Conceptually, though, it just doesn't feel right.
I suppose if you work in an organization where everyone (including management) is very disciplined and respects the high level architecture then this isn't much of a benefit, but I've never had the pleasure of working in such an organization.
That said, I hear all the time about people making a mess with micro services, so I'm sure there's another side, and I haven't managed to figure out yet why these other experiences don't match my own. I've mostly seen micro service architecture as an improvement to my experiences with monoliths (in particular, it seems like it's really hard to do many small, frequent releases with monoliths because the releases inevitably involving collaborating across every team). Maybe I've just never been part of an organization that has done monoliths well.
This fallacy is the distributed computing bogeyman.
Just because you peel off a responsibility out of a monolith that does not mean you suddenly have a complex mess. This is a false premise. Think about it: one of the first things to be peeled off a monolith are expensive fire-and-forget background tasks, which more often than not are already idempotent.
Once these expensive background tasks are peeled off, you gets far more manageable and sane system which is far easier to reason about and develop and maintain and run.
Hell, one of the basic principles of microservices is that you should peel off responsibilities that are totally isolated and independent. Why should you be more concerned about the possibility of 10% of your system being down if the alternative is having 100% of your system down? More often than not you don't even bat an eye if you get your frontend calling your backend and half a dozen third-party services. Why should it bother you if your front-end calls two of your own services instead of just one?
I get the concerns about microservicea, but this irrational monolith-mania has no rational basis either.
Personally I would like to try Umbrella Projects[1]. You can design it as microservices but deploy and build as monolith. Overhead is lower, and it is easier to figure out right services when in one codebase. It can be easy implemented in other lang/frameworks as well.
[1] https://elixirschool.com/en/lessons/advanced/umbrella-projec...
^ Compile times are fast so this isn't an issue like it can be in other languages.
AFAIK, we haven't actually split out anything yet after all these years. All of our scale issues have been related to rather lackadaisical DB design within services (such as too much data and using a single table for far too many different purposes), something an arbitrary HTTP barrier erected between services would not have helped with at all.
The biggest downside I've encountered is that you need to figure out the deployment abstraction and then figure out how that impacts CI/CD. You'll probably do some form of templating YAML, and things like canaries and hotfixes can be a bit trickier than normal.
No it can't, it relies on the runtime being capable of fairly strong isolation between parts of the same project, something that is a famous strength of Erlang. If you try to do the same thing in a language like Python or Ruby with monkeypatching and surprise globals, you'll get into a lot of trouble.
People are talking about Conway’s law, but the more important one here is Gall’s law: “A complex system that works is invariably found to have evolved from a simple system that worked.”
A million times this. If you can't chart the path to fixing what you have, you don't understand the problem space well enough.
The most common reply I've heard to this is "but the old one was in [OLD FRAMEWORK] and we will rewrite in [NEW FRAMEWORK OR LANGUAGE]," blaming the problem not on unclear concepts/hacks or shortcuts taken to patch over fuzzy requirements but on purely "technical" tech debt. But it usually takes wayyyyy longer than they expect to actually finish the rewrite... because of all the surprises from not fully understanding it in the first place.
So even if you want the new language or framework, your roadmap isn't complete until you understand the old one well enough.
I am working on a project right now that was designed as microservices from the start. It’s really hard to change design when every change impacts several independent components. It seems to me that microservices are good for very stable and well understood use cases but evolving a design with microservices can be painful.
* Make additional changes in B which also takes resources, times and introducing overhead + point of failure, or
* Make A interact directly with C which breaks the boundary
The source of most of our problems is always that we started too small to understand the implication of our laziness at the start (I'll loop over this thing - oops it's now a nested loop with 1 million elements because a guy 3 years ago made it nested to fix something and the business grew). Most times, we simply have to profile / fix / profile / fix until we reach the sub millisecond. Then we can discuss strategic architecture.
Interestingly most of the architecture problem we actually had to solve were because someone 20 years ago chose an event-based micro service architecture that could not scale once we reach millions upon millions of event and has no simple stupid way to query state but to replay in every location the entire stream. Every location means also the C# desktop application 1000 users use. In this case yes, we change the architecture to have basic indexed search somehow with a queriable state rather than a reconstructed one client-side.
Well sometimes there are very complex subsystems in the monolith and it's easier to create a completely new microservices out of that instead of trying to rewrite the existing code in the monolith.
We had done so successfully by creating a new payment microservices with stripe integration and then just route every payment that way. Doing the same in a huge pile of perl mess has been assessed as (nearly) impossible by the core developer team without any doubts.
But I have to admit that the full monolith code base is in a maintenance mode beyond repair, only bug & security fixes are accepted at this point in time. Feature development is not longer a viable option for this codebase.
Often, the problems with the monolith are
* different parts of the monolith's functionality need to scale independently, * requirements for different parts of the monolith change with different velocity, * the monolith team is too large and it's becoming difficult to build, test, and deploy everyone's potentially conflicting changes at a regular cadence, with bugs in one part of the code blocking deploying changes to other parts of the code.
If you don't have one of these problems, you probably don't need to break off microservices, and just fixing the monolith probably makes sense.
This is the only argument I'm ever really sold on for an SOA. I wonder if service:engineer is a ratio that could serve as a heuristic for a technical organization's health? I know that there are obvious counter-examples (shopify looks like they're doing just fine), but in other orgs having that ratio be too high or too low could be a warning sign that change is needed.
I hear this a lot. Can you give me an example? Can't I just add more pods and the "parts" that need more will now have more?
Agreed on the other reasons (although ideally the entire monolith should be CI/CD so release cadence is irrelevant, but life isn't perfect)
Some of this is driven by well documented cognitive biases, such as the availabily heuristic which you've identified, but there are so many.
This is a good video on it https://birgitta.info/redefining-confidence/
I'd like the idea of vertical slices for this: once you've done this, then refactor to microservices will be much easier.
Somehow mentioning that microservices might not be perfect solution in every case triggers a lot of people.
I have actually helped save at least one project in a huge bank which got rolled from 140 services into one. The team got also scaled down to third of its size but was able to work on this monolithic application way more efficiently. Also reliability, which was huge issue before the change, improved dramatically.
When I joined, I have observed 6 consecutive failed deployments. Each took entire week to prepare and entire weekend to execute (with something like 40 people on bridge call).
When I left I have observed 50 consecutive successful deployments, each requiring 1h to prepare (basically meeting to discuss and approve the change) and 2h of a single engineer to prepare and execute using automation.
Most projects absolutely don't need microservices.
Breaking anything apart brings inefficiencies of having to manage multiple things. Your people now spend time managing applications rather than writing actual business logic. You have to have really mature process to bring those inefficiencies down.
If you want to "do microservices" you have to precisely know what kind of benefits you are after. Because the benefits better be higher than the costs or you are just sabotaging your project.
There are actually ways to manage huge monolithic application that don't require each team to have their own repository, ci/cd, binary, etc.
How do you think things like Excel or PhotoShop have been developed? It is certainly too large for a single team to handle.
If you have trouble managing ONE application what makes you think you will be better at managing multiple?
Also, running distributed system is way more complicated than having all logic in a single application. Ideally you want to delay switching to distributed system until it is inevitable that you are not going to be able to fulfill the demand using monolithic service.
If your application has problems, don't move to microservices. Remove all unnecessary tasks and let your team focus on solving the problems first and then automate all your development, testing, deployment and maintenance processes.
Or call me and I can help you diagnose and plan:)
I also find that by having separate distinct services, it puts up a lot of friction to scope creep in that service and also avoids side effect problems- IE you made this call, and little did you know this updated state somewhere you completely didn't expect and now touching this area is considered off limits, or at least scary because it has tentacles in so many different places. Eventually this will absolutely happen IME. No of course not on your team, you are better than that, but eventually teams change, this is now handled by the offshore or other B/C team, or a tyrant manager takes over for a year or two before that is obsessed with hitting the date, hacks or not, etc...
But I guess an absolutely critical key to that is having a logging/monitoring/testing/tracing workflow built in. Frameworks can help, Hapi.js makes a lot of this stuff a core concept for example. This is table stakes to be doing "micro" services though and any team that doesn't realize that has no business going near them. Based on the comments here though ignorance around this for teams embracing microservices might be more common than I had imagined.
Microservices make sense when you have millions of users and there is a need to quickly scale horizontally. Or when you have a zillion of developers which probably means that your product is huge. Or when you are building a global service from the get go and get funded by a VC.
Would be interested to hear about some of these.
You divide your application into separate problems each represented by one or more modules. You create API for these modules to talk to each other.
You also create some project-wide guidelines for application architecture, so that the modules coexist as good neighbors.
You then have separate teams responsible for one or more modules.
If your application is large enough you might consider building some additional internal framework, for example plugin mechanism.
For example, if your application is an imaginary banking system that takes care of users' accounts, transactions and products they have, you might have some base framework (which is flows of data in the application, events like pre/post date change, etc.) and then you might have different products developed to subscribe to those flows of data or events and act upon the rest of the system through internal APIs.
Each "Microservice" could live in a separate package which you can import and bundle into single executable.
Elixir has "Umbrella Projects": https://elixirschool.com/en/lessons/advanced/umbrella-projec...
Rust/Cargo has workspaces: https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html
The build of a component would only have access to the API's of the other components (and this can include not having knowledge of the container it runs in).
The implementation can then change rapidly, with the API that the other teams develop against moving more slowly.
Even so, code reviews can be critical. The things to look out for (and block if possible) are hidden or poorly defined parameters like database connections/transactions, thread local storage and general bag parameters.
In some languages dependency injection should be useful here. Unfortunately DI tools like Spring can actually expose the internals of components, introduce container based hidden parameters and usually end up being a versioned dependency of every component.
Ex Amazon SDE here. I've been saying many times that Amazon tends to have the right granularity of services: roughly 1 or 2 services for a team.
A team that maintains the service in production autonomously and deploys updates without having to sync up with other teams.
[Disclaimer: I'm not talking about AWS]
"So let me get this straight, you have a Billing package and an Ordering package. How are these organized again?"
-> "We have a distributed system."
"So you you compile them together and deploy them horizontally across many machines? Like a distributed monolith?"
-> "No we use microservices. The two packages are compiled and deployed separately so there is no code dependencies between them."
"Okay. So if I understand this right, you develop each package separately but they both access the same database?"
-> "No no no no! Microservices are not just about code dependency management, they are also about data ownership. Each package is developed and deployed separately and also manages it own database."
"Ah, so you have two monoliths?"
-> "..."
You see the problem is that the above terms are too broad to describe anything meaningful about a system. And usually "monolith" is used to described the dependencies between code whereas "microservices" is used to describe dependencies about data. It turns out there is a lot of overlap.
Design and implement your systems according to your use-case and stop worrying about how to label them.
Made me snort.
A: We have a problem. I know, we'll use divide and conquer! Runs off.
B: Uh, that's actually Banach-Tarski^W^Wmicroservices? Wait, come back!
later
B: Now we have two problems, contorted to fit into a spherical subspace^W^WIO monad, and a dependency on the axiom of choice^W^W^Wasyncio library.
A: Wait, why are there monads? This is Javascript!
B: There are always monads, it's just a question of whether you type system is overengineered anough to model them.
Things can get tricky if you need to spread out to hundreds of machines, but 99%+ of projects wont get to that scale.
[1] https://elixir-lang.org/blog/2016/07/14/announcing-genstage/
https://blog.appsignal.com/2018/10/16/elixir-alchemy-hot-cod...
- from Erlang Programming by Simon Thompson, Chapter 1
Now the trend of breaking things up for the sake of it - going micro - seems to benefit the cloud, consultancy and solution providers more than anybody else. Orchestrating infrastructure, deployments, dependencies, monitoring, logging, etc, goes from "scp .tar.gz" to rocket science fast as the number of services grows to tens and hundreds.
In the end the only way to truly simplify a product's development and maintenance is to reduce its scope. Moving complexity from inside the code to the pipelines solves nothing.
The company's design philosophy is based more on what is fashionable that what is suitable.
The plan worked.
I worked for a while at a small startup, initially as the sole developer and eventually growing to 3. We had an architecture I would describe as at least being "microservices-esque": a single large "monolithic" backend service, with a number of smaller consumer services, webapps, UIs.
The distinction between microservices and monoliths may be debatable but I believe monoliths as described by Martin Fowler typically involve your primary service logic and your UI coexisting. I've found that splitting these at least is usually fruitful.
I think this approach is sometimes called "fan-out", though I've seen that term used to describe other different things too; namely the opposite where there's a single monolithic UI layer fanning out to many micro upstreams.
TL;DR: Fowler's opening 2 bullet points seem likely to be a false dichotomy as he's force-classifying all architectures into two buckets.
I've worked at several companies where microservices were necessary, and I can't believe how clunky they are to work with. I feel like in 2021 I should be able to write a monolith that is capable of horizontally scaling parts of itself.
And regarding the cost you mention, I don't think there is any. Yes, the components only know each other via their interfaces, not their concrete types. Yes, you need to define these interfaces. But that's in essence plain old school dependency injection. You'd do that anyway for testing, right?
In my experience the cost is lower if you write code the way you described (and you don't need to plan ahead as much)
https://gobyexample.com/interfaces
So a struct satisfies an interface if it implements all of the methods of the interface. The example you see in the above link is a monolithic design. Everything only exists in one place and will be deployed together.
So if the functions for circle were getting hit really hard and we wanted to split those out into their own microservice so that we could scale up and down based on traffic, it would look something like this. https://play.golang.org/p/WOp0RL-pVg3
There we have externalCircle which satisfies the interface the same way circle did, but externalCircle makes http calls to an external service. That code won't run because it's missing a thing or two, but it shows the concept.
I'd wager, something like 90% of software projects in companies can just get by with monoliths.
You know...there are monoliths that are well architected, and then there are monoliths that are developed for job security.
Can you share one example where an error couldn't be handled gracefully in a monolith?
Say if my shipping service fails for some reason, I can still fallback to a default cost in a monolith.
What do you mean? Aside from deployed codebase, I don't quite see it - if you need memory, allocate - start small/empty for any datastructure. If managed/gc setup delallocating is inherent, so no special case about freeing memory. Don't create unnecessary threads - but even then dormant threads are very cheap nowadays. There you go - it scales vertically nicely, make sure the application can scale horizontally (say, partitioning by user) and you have all the benefits with close to no drawbacks
Okay, we've got a php process. Let's put our business logic there.
Okay, I need a database. With current cloud practices, that's probably going to be deployed in a different machine. We've got our business logic service and our datastore separated by a network bound from the get go.
Okay this is not very performant, I need to cache some data. Let's add a redis instance in our cloud provider. I guess this we'll be our cache service.
Okay we need to do full-text search, let's add an elasticsearch cluster through our cloud provider.
Okay I need to store users. We can built that into our business logic core, of course, but screw that. We are using Auth0. I guess our "user service" is distributed now. Okay, my php process can't keep up with all this requests, let's scale this horizontally by deploying multiple php processes behind a load balancer!
Okay, now I need to do some batch processing after hours. I could add a cron job for that, but I don't want to deal with everything needed for retrying/reprocessing/failure handling plus now that it's a multi-instance deployment it's not even clear which process should do this! Let's put this work in a Lambda from our cloud provider.
So until now, we had a star architecture where everything talked to our business core. But adding this lambda that will talk directly to other services without consulting the core we've lost that.
Now, stripping out the business core into its constituent parts doesn't sound so strange, does it?
My setup: I have async worker running alongside my application. Same codebase, same environment, same monolith, just instead of accepting HTTP requests it pops tasks off a queue. The framework handles retrying/reprocessing/failure.
To run cron tasks, I have CloudWatch Events -> SNS -> HTTP endpoint in the monolith which pushes the cron task into the queue.
And this isn't some unusual setup. This is something that people have been dealing with for a very long time, long before Lambda came into existence. Most web frameworks have async tasks built-in or an integration with a framework that can do async tasks.
Now you need to upgrade to a new version, which requires an incompatible DB migration affecting the request handling and the hourly job. The long job started at 2PM. How do you do the upgrade, without someone needing to stay late?
(If it's e.g. PHP this gets even worse due to lazy package loading, you don't need a DB migration just a changed import path.)
I'm not a fan of microservices, especially the ridiculous factoring I see in lambda-style approaches today. But one of our biggest PITAs was a "horizontally scalable monolith" that after 7 years has accrued so so many responsibilities it's either impossible to find a time slot for an upgrade with zero/low downtime, or we have to plan & develop dealing with shared storage where half may upgrade one day and half the next - all the operational overhead of SOA without the benefits.
(My holy grail would be programming for distributed environments as if you had one giant single-thread computer, and function calls could arbitrarily happen over IO or not, then concerns would boil down to code organisation first. I believe Erlang's OTP model or the way some features of Google Cloud Platform are organised gets us closer to this ideal, but we're not quite there yet.)
Ex-Amazon SDE here. Message-passing libs like 0mq tried to push this idea.
They never became very popular internally because passing data between green threads vs OS threads vs processes vs hosts vs datacenters is never the same thing.
Latencies, bandwidths and probability of loss are incredibly different. Also the variance of such dimensions. (Not to mention security and legal requirements)
Furthermore, you cannot have LARGE applications autonomously move across networks without risks of cascading failures due to bottlenecks taking down whole datacenters.
Often you want to control how your application behaves on a network. This is also a reason why OTP (in Erlang or implemented in other languages) is not popular internally.
In that architecture you don’t need that many Microservices. The api server is prolly the most important bit and can be served as a monolith.
Many 10B+ companies use this architecture. It works well and does the job. Mono/Microservices is really about separation of concerns. Separation of concerns mostly around what you want to deploy as a unit, redundancy and what should be scaled as a unit.
Agree with author. Start with one chunk, and split when you feel the pain and need that separation of concern. Fewer pieces are easier to reason about.
Monolith -> Microservices -> Monolith
The first monolith was so bad that microservices made sense at that point in time. The current monolith is an exemplar of why we do not use microservices anymore. Everything about not fucking with wire protocols or distributed anything is 100x more productive than the alternatives.
Deciding you need to split your code base up into perfectly-isolated little boxes tells me you probably have a development team full of children who cannot work together on a cohesive software architecture that everyone can agree upon.
I'm working on a project right now trying to spin up the infrastructure in the public cloud for a simple application that some bored architecture astronaut decided would be better if it had an "API tier". No specific reason. It should just have tiers. Like a cake.
Did you know Azure's App Service PaaS offering introduces up to 8-15ms of latency for HTTPS API calls? I know that now. Believe, me I know that all too well.
To put things in perspective, back in the days I cut my teeth writing "4K demos" that would be added to zip files on dialup bulletin boards. In those days, I carefully weighed the pros and cons of each function call, because the overheads of pushing those registers onto the stack felt unnecessarily heavyweight for something that's used only once or maybe twice.
These days, in the era of 5 GHz CPUs with dozens of cores, developers are perfectly happy to accept RPC call overheads comparable to mechanical drive seek times.
I can hear the crunching noises now...
If you have a monorepo, and you make a change to a schema (backwards compatible or not, intentional or not), it's a lot easier to catch that quickly with a test rather than having a build pipeline pull in dozens of repos to do integration tests.
Also, if you have a bunch of services all sharing the same schema, you aren't really doing microservices. Not because of some No True Scotsman, "Best practices or GTFO," sentiment, but because that design question, "Do services only communicate over formal interfaces, or do we allow back channels?", is the defining distinction between SOA and microservices. The whole point of the microservices idea was to try and minimize or eliminate those sorts of tight coupling traps.
A problem I've seen on the project I'm working on is that the team seems to want to do conflate orthogonal issues from a design and technical perspective. "We're going to microservices because the monolith is slow and horrible. We're going to use CQRS to alleviate pressure on the repositories. We're going to use Event Sourcing because it works nicely with CQRS."
Question: "Why are we Event Sourcing this simple CRUD process?"
Answer: "Because we are moving from the Monolith to Microservices".
You get dependency cycles when two dependent services need the same thing and you lack a good place to put that thing. You start with modules A and B. Then B need something that A has and then A needs something that B has. You can't do it without introducing a dependency cycle. So, you introduce C with the new thing. And A and B depend on C but B still also depends on A. And so on.
True weather you do Corba, COM, SOAP, Web RPC, OSGi, Gradle modules, etc. The only difference is the overhead of creating those modules is different and has varying levels of ceremony, management needs, etc. Also refactoring the module structure gets more complicated with some of these. And that's important because an organically grown architecture inevitably needs refactoring. And that tends to be a lot more tedious once you have micro services. And inevitably you will need to refactor. Unless you did waterfall perfectly and got the architecture and modularization right in one go. Hint: you won't.
Finally, the same kind of design principles you use for structuring your code (e.g. SOLID, keeping things cohesive, maximizing cohesiveness, Demeter's law, etc.) also applies to module design. Services with lots of outgoing dependencies are a problem. Services that do too much (low cohesiveness are a problem). Services that skip layers are a problem. The solutions are the same: refactor and change the design. Except that's harder with micro-services.
That's why Martin Fowler is right. Start with a monolith. Nothing wrong with those and should not stop you practicing good design. Using microservices actually makes it harder to do so. So, don't introduce microservices until you have to for a good technical or organizational reason (i.e. Conway's law can be a thing). But don't do it for the wrong reason of it being the hip thing to do.
With a monolith, you change everything, create 1 commit. And after that passes CI/CD it can be live in minutes.
takes a request
-> deserializes it/unpacks it to a function call
-> sets up the context of the function call (is the user logged in etc)
-> calls the business logic function with the appropriate context and request parameters
-> eventually sends requests to downstream servers/data stores to manipulate state
-> handles errors/success
-> formats a response and returns it
The main problem I've seen in monoliths is that there is no separation/layering between unraveling a requests context, running the business logic, making the downstream requests, and generating a response.Breaking things down into simplified conceptual components I think there is a: request, request_context, request_handler, business_logic, downstream_client, business_response, full_response
What is the correct behavior?
return request_handler(request):
request -> request_context;
business_response = business_logic(request_context, request):
downstream_client();
downstream_client();
business_response -> full_response;
return full_response;
business_response = request_handler(request_context, request):
return business_logic(request_context, request):
downstream_client();
downstream_client();
business_response -> full_response;
return full_response;
request -> request_context;
business_response = request_handler(request_context, request, downstream_client):
return business_logic(request_context, request, downstream_client):
downstream_client();
downstream_client();
business_response -> full_response;
return full_response;
something else?
In most monoliths you will see all forms of behavior and that is the primary problem with monoliths. Invoke any client anywhere. Determine the requests context anywhere. Put business logic anywhere. Format a response anywhere. Handle errors in 20 different ways in 20 different places. Determine the request is a 403 in business logic, rather than server logic? All of a sudden your business logic knows about your server implementation. Instantiate a client to talk to your database inside of your business logic? All of a sudden your business logic is probably manipulating server state (such as invoking threads, or invoking new metrics collection clients).The point at which a particular request is handed off to the request specific business logic is the most important border in production.
This just seems like a poorly (or not at all?) designed monolith, if there's no standard way of doing things, of concerns or responsibilities of various application layers? I mean I've been there too in organizations, but it just seems like we're skirting around the obvious: the system should've had a better architect (or team) in charge?
First you need to have people who understand architecture. College does not meaningfully teach architecture. How many businesses are started with senior devs who know what they are doing? How many business are going to spend time on architecture while prototyping? When a prototype works, do you think they are going to spend resources fixing architecture or scaling/being first to market?
When a new employee joins, how many companies are going to inform the new employee on standard architecture practices for the company? After how many employees do you think it's impossible for 1 person to enforce architecture policy? Do you think people will even agree what best architecture is?
What about hiring new people? Is it important to hire another dev as fast as possible when you get money, or to have 1 dev fix the architecture? After all technical debt is cheaper to pay off (50% of engineering resources) with 2 devs than with 1 (100% of engineering resources), context switching is it's own expense...
Once you get into pragmatics you understand that good architecture is a common in the tragedy of the commons sense. It takes significant resource cost and investment for a very thankless job. So you must have authority make a commitment to architecture, who is almost always going to be making cost benefit analysis which is almost always going to favor 1 day from now to 1 year from now.
How do you make graphs that make sense of your various microservices? How do you make sure code doesn't get stale/all versions are compatible/rolling back code doesn't take out your website? How do you do capacity planning? How are service specific configurations done? How does service discovery work? How do you troubleshoot these systems? How do you document these systems? How do you onboard new people to these systems? What about setting up integration testing? Build systems? Repositories? Oncall? What happens when a single dev spins up a microservice and they quit? What happens when a new dev wants to create a new service in the latest greatest language? What happens when you want to share a piece of business logic of some sort? What about creating canaries for all these services? What about artifact management/versioning?
What about when your microservice becomes monolithic?
Complexity is complexity no matter where it exists. Microservices are an exchange of one complexity for another. Building modular software is a reduction of complexity. A microservice that has business logic mixed with server logic is still going to suffer from being brittle.
have seen that happening. importing modules from all over the place to get a new feature out in the monolith. also, note this happened with rapid on boarding, the code-review culture was not strong enough to prevent this.
so the timing of the change from monolith to micro-service is critical, in order to get rid of it. otherwise, chances are high you got a monolith and microservices to take care of.
I've been working on something that tries to make starting with a monolith in React/Node.js relatively easy. Still don't have much of the "peeling" support but that is something we're looking to add: https://github.com/wasp-lang/wasp
The design challenge becomes making sure that your modules can execute within the monolith or across services. The work to be done can be on a thread level or process level. The interfaces or contracts should be the same from the developers point of view. The execution of the work is delegated to a separate framework that can flip between thread and process models transparently without extra code.
This is how I approached my last microservices project. It could be built as one massive monolith or deployed as several microservices. You could could compose or decompose depending on how much resources where needed for the work.
I fail to understand why techniques around these approaches aren't talked about in detail. They have some degree of difficulty in implementation but are very achievable and the upsides are definitely worth it.
I think however defining these modules and the interfaces between them is the hard part. Part of this work is defining the bounded contexts and what should go where. If I understand DDD correctly this shouldn't be done by «tech» in isolation. It's something that's done by tech, business and design together. This is hard to do in the beginning – and I would argue that it should not be done in the beginning.
When starting out you've just got a set of hypothesis about the market, how the market wants to be adressed and in which way you can have any turnover doing it. This isn't the point when one should be defining detailed bounded contexts, but should instead just be experimenting with different ways to get market fit.
Edit: typo
[1] source - https://github.com/Imaginea/Inai
[2] blog post describing Inai - https://labs.imaginea.com/inai-rest-in-the-small/
PS: I've had some difficulty characterising Inai (as you can tell).
This way you get most of the benefits of separating a codebase (such as testing different things, leaving code that barely works in the repo, pointing to older versions of part of the monolith, smaller repos, etc.) whilst integration between modules is a one-liner, so I don´t need microservices, servers, information transformation, etc.
There's even the possibility to dynamically add library requirements with add-lib, an experimental tools.deps functionality.
I've built native C++ server that does some complex computations in some business domains.
When this monolith exposes generic JSON RPC based API where agents (human or software) can connect and do various tasks: admin, business rules design and configuration, client management, report design etc. etc. based on permission sets.
Now actual business client came. They have their own communication API and want integration with our server and of course being "customer is a king" they want communications tailored to their API. I was warned that this customer is one of many more that company is signing deals with. Not gonna put Acme specific code into main server.
This is where microservice part comes. Just wrote a small agent that mediates between client API and ours. Not much work at all. Come another client I can add more specific parts to this new microservice, or give it to other team to build their own based on first one as a template.
Physical scalability - I do not worry about. We re not Google and will never be due to nature of the business. Still main server can execute many 1000s of requests/s sustainably on my laptop (thank you C++). Put it on some real server grade hardware and forget about all that horizontal scalability. Big savings.
I remember, a little more than 4 years ago I was brought on to save a product launch for a startup. They were, as far as I can remember, already creating the 2nd iteration/rewrite of their MVP with the first one never released and just few months short of doing their first release one of their developers started to convince everyone that the code base was a pile of crap and that they need to rebuild the whole thing is microservices. Because there is no other way around. The system was built in PHP and the otherwise smart and motivated developer wanted to rebuild it as a set of JS microservices.
He almost managed to convince the management, including the devlopment lead and the rest of the team. And it wasn't easy to convince him that that would have been a pretty dumb move and that it wouldn't have just taken a few more months, just because creating a 'user service' with a mongo backend was something he could pull of in his spare time.
Then I realized that there is something inherent in these kind of situations. Some people come around stating that a new piece of technology (or other knowledge) is better and then suddenly the rest of the world is somehow on the defense. Because they know something the rest don't so how could you prove them wrong? Not easy. And funny enough, this is actually a classic case of the shifting of the burden of proof fallacy.
So they decided to break it up into multiple services. First was a pdf report generation service which I wrote in node.js. Then more and more services were added and other other modules were ported as node apps or separate java apps. In the end it was around 12 services and all of them worked well.
The monolith was still there but it's fast enough and users were happy. That's a lesson I'll never forget and have understood that time and velocity are far more important for a product to succeed!
[0] https://microservices.io/patterns/refactoring/strangler-appl...
The other was an attempted rebuild of an existing .NET application to a Java microservices / service bus system. I think there was no reason for a rebuild and a thorough cleanup would have worked. If that one did not move to the microservices system, the people calling the shots would not have a leg to stand on because the new system would not be significantly better, and it would take years to reach feature and integration parity.
Requirements should dictate architecture. Data should dictate the model. Avoid unnecessary work. Be mindful of over-engineering.
I'm not from the camp that believes in creating services for every single thing - It's the same camp that believes in starting with distributed databases and then moving in the same direction. I believe in PostgreSQL everything and then moving to distributed only if the application demands ... Wait did I just start another war in here !
I've worked on workflow/crud-type web applications whose computational needs could be met using a single server with a couple of GB of RAM using a single database, indefinitely into the future. I don't see why it would occur to someone to split one of those into multiple services.
I've worked on systems for which many such servers were required in order to provide the expected throughput and, in such a case, writing a monolith is really not an option.
Is there a significant middle ground?
For me, the issue is that a monolith based on, for example, Apache Tomcat using background work threads let’s me also handle background tasks in one JVM. I have not used this pattern in a long time, but when I did it served me well.
I think Java and the JVM are more suitable for this than Python and a web framework. I tried this approach with JRuby one time using background worker threads but I wouldn’t use CRuby.
This is somewhat reminiscent of the abuse of higher level languages and the mindset computers are so powerful there's no need to think too hard. However, the consequences are no longer limited to a slow and buggy program but many slow and buggy programs and a large AWS bill too!
Then and only then should you even consider paying back your technical debt. Monolith first always until you have the cash to do otherwise, and even then only if your app desperately needs it.
But if you are a 300 person organization launching something from the ground up, I would choose many serverless solutions over a single monolith.
I think this is one of the most important points. Often it takes time to figure out what the right boundaries are. Very rarely do you get it right a priori.
Would they have kept them for themselves (including k8s), would microservices be just as fashionable? I'd really like to know the answer, while watching my friends startup of 4 people running they 14 microservices on a k8s cluster.
It is not something that should be used simply to clean up your code. You can refactor your code in any way you see fit within a monolith. Adding a network later in between your modules does not make this task simpler.
Once that is done, I work on each module as a standalone project, with its own lifecycle.
I’m also careful not to break things up “too much.” Not everything needs to be a standalone module.
There exist other ways of designing 'microservices' that are not necessarily conventional monoliths!
Why swim against a tide of industry “best practice” that says ...
... Lets make our application into many communicating distributed applications. Where the boundaries lie is unclear, but everyone says this is the way to produce an application (I mean applications), so this must be the way to go.
I am very interested in what he could add now.
You start out as technically a monolith, but that is prepared at all times to be decomposed into services, if and when the need arises.
It's nothing too fancy - can be simply another name for Hexagonal Architecture, functional-core-imperative-shell, etc.
The value of microservices is to isolate risks and have some manageable entity to reason about, to have an understandable scope of lifecycle, test coverage and relations with other services. The larger the piece, the harder it is to do.
Splitting up is normally harder than building up, and often impossible to agree on. Any monolyth I worked on was coupled more than necessary because of regular developer dynamics to use all code available in classpath. I've never even saw a successful monolyth split - you just usually rewrite from scratch, lift-n-shifting pieces here and there.
The point of this idea isn't that it says "monolith"; it's that it includes time as a factor. Too much of our discussion focuses on one state or another, and not on the evolution between states.
> The logical way is to design a monolith carefully, paying attention to modularity within the software, both at the API boundaries and how the data is stored. Do this well, and it's a relatively simple matter to make the shift to microservices.
This is the big take-away to me -- for a long while now I've seen this whole monoliths-vs-microservices debate as a red herring. Whether your conduit is functions in shared virtual address space, HTTP or a Kafka topic the real problem is designing the right contracts, protocols and interface boundaries. There's obviously an operational difference in latencies and deployment difficulty (i.e. deploying 5 separate apps versus 1) but deep down the architectural bits don't get any easier to do correctly, they just get easier to cover up (and don't sink your deployment/performance/etc) when you do them badly when there's less latency.
What we're witnessing is a dearth of architectural skill -- which isn't completely unreasonable because 99% of developers (no matter the inflated title you carry) are not geniuses. We have collectively stumbled upon decent guidelines/theories (single responsibility, etc), but just don't have the skill and/or discipline to make them work. This is like discovering how to build the next generation of bridge, but not being able to manage the resulting complexity.
I think the only way I can make the point stick is by building a bundle of libraries (ok, you could call it a framework) that takes away this distinction -- the perfect DDD library that just takes your logic (think free monads/effect systems in the haskell sense) and gives you everything else, including a way to bundle services together into the same "monolith" at deployment time. The biggest problem is that the only language I can think of which has the expressive power to pull this off and build bullet-proof codebases is Haskell. It'd be a hell of a yak shave and more recently I've committed to not spending all my time shaving yaks.
Another hot take if you liked the one above -- Domain Driven Design is the most useful design for modern software development. The gang of 4 book can be reduced to roughly a few pages of patterns (which you would have come across yourself, though you can argue not everyone would), and outside of that is basically a porn mag for Enterprise Java (tm) developers.
Well, yeah... obviously. That's not the same thing as it being bad to start with microservices generally.
I just don't agree with this at all. The designer of the architecture clearly does need to know how to design service-based architectures and needs to have a very strong understanding of the business domain to draw reasonable initial boundaries. These are not unusual traits when starting a company as a technical founder.
In a monolith, no biggy. With microservices, huge pain.
Moving to microservices is something you do to optimise scability, but it comes with costs. A major cost is the reduction in flexibility. Starting a new not-very-well-understood thing needs massive flexibility.
Its actually not unusual to have technical cofounders who have no prior software engineering work experience.
Looking at the page now it looks like the monolith is still running as far as I can see, about 5 years later. I guess they gave up. :)
Martin is definitely ahead of the curve, or perhaps I'm behind.
> Although the evidence is sparse, I feel that you shouldn't start with microservices unless you have *reasonable experience of building a microservices system* in the team.
I proposed and designed a micro services architecture with the team. I had done that sort of thing a few times even before they were called micro services. There were a few advantages: * The product team had the freedom to re-think the value proposition, the packaging, the features. It was critical for the business in order to remain competitive. * The development team could organize in small sub teams with natural affinity to the subdomains they had more experience/expertise in. * Each domain could progress at the pace that fits, with versioned APIs ensuring compatibility. Not necessarily a unique micro service success prerequisite. One can argue versioning APIs is a good best practices even for internal APIs but the reality is that versioning internal APIs is often less prioritized or addressed to begin with.
There are technical pros and cons for monolith/MS. Additional data that can augment this decision is the org structure , the teams dynamic and the skillsets available. In the case of that project, the team had excellent discipline around testing and CI/CD. Of course there are challenges. Integration testing becomes de-facto impossible locally. Debugging is not super difficult with the right logging, but still harder. One challenge I saw with that project and other projects that adopt microservices is that the way of thinking switch to 'ok so this should be a new service'. I think this is a dangerous mindset, because it trivializes the overhead of what introducing a new service means. I have developed some patterns that I call hybrid architecture patterns, and they have served me well.
One thing to consider when deciding what road to take, is how does the presentation layer interact with the micro services. When it is possible to map the presentation to a domain or a small subset of the domains, the micro services approach suffers less from the cons. A monolithic presentation ---> Micro services backend could severely reduce benefits.
2 good references here: 'Pattern oriented software architectures' 'Evaluating software architecture'
Generally, focus on solving the problems first.
Can the problem be solved by some static rendering of data? Monolith is better solution.
Does the problem require constant data updates from many sources and dynamic rendering of data changes? Micro services and reactive architecture are better solutions.
There’s no one size fits all solution. The better software engineers recognizes the pros and cons of many architecture patterns and mix match portions that make sense for the solution, given schedule, engineering resources, and budget.
Monolith vs microservice IMO depends more on team size/structure, stage of the project (POC/beta/stable/etc.), how well defined the specs are, … Rather than the nature of the problem itself.