My crazy advice: Build a monolith as microservices.
By which I mean focus on Domain Driven Development, focus strongly on modularity even when, and this is the key part, it's not required because it's in a monolith.
There are a great deal of advantages to microservices like teams being able to focus on a subset that can still be accomplished with a well designed monolith. While still avoiding the biggest headaches with microservices like managing a crazy network of deployments, across multiple environments where teams don't have the neccessary discipline to communicate.
If at a certain point scalability does become a big enough issue, then it's much more manageable to segment what's needed (and even transition back if needed). It still might take months, but the quality of the transition is much higher.
By which I mean focus on Domain Driven Development, focus strongly on modularity even when, and this is the key part, it's not required because it's in a monolith
Here, here! If I had a dollar for every team I’ve seen adopt micro services as a solution to code modularization I’d have several dollars.
“Hey our code could be cleaner and our git repo seems to be getting big”
“Sweet let’s inject the network in between everything”
Having lived between packaged based repos (Amazon) and mono repo (FB), I've come to greatly appreciate the mono repo life. All my dependencies match up, and I can build everything in one command. Furthermore, I can run all 4007 tests in 13 minutes.
Agreed! I worked at a job where most of the company worked on a monolith, with the frontend in its own repo, then another 25 microservices hanging off the monolith. Everything was different in every repo, including how to deploy and run tests etc.
The project I was working on was everything in one repo with a nice deploy.sh and test.sh and no chasing people to find out how to deploy that one microservice that was last touched months ago.
Cloud provider's best trick was to convince everyone that network calls are better than function calls.
Not really. Cloud providers did convince everyone that if you hit a resource limit in one of your boxes then your best strategy is being able to add more boxes, and not move everything and the kitchen sink to a larger box.
Also, not everything sits in the hot path of anything. Sometimes all you want is a long-running job to run somewhere.
* Network calls are traceable by default — logging all incoming queries is easy, logging all function calls is harder.
* Network calls are easily made — function calls in a backend can require passing some kind of a huge config structure, or might be entirely unavailable if you're using a compiled language, while network calls can be done with nothing more than curl/grpcurl/etc.
The downsides are a) decreased performance b) you have to handle failures c) dev ergonomics, but eh. I would still choose network calls over function calls for a big backend, probably.
It's really not though. Many (most?) languages have some way to annotate function calls with logging logic
function calls in a backend can require passing some kind of a huge config structure, or might be entirely unavailable if you're using a compiled language
Completely disagree if you work on low latency systems, or a high volumes of requests: functions are traceable if you need to in a crisis: plug a profiler, run like prod, see the hot paths.
Dev ergonomics explode the cost of everything and you spend your time trying to figure out what the hell is going on where rather than just... plugging the profiler and sending the input, then observe the internals moving around.
If the system can work without micro services, it should.
Network calls are easily made — function calls in a backend can require passing some kind of a huge config structure, or might be entirely unavailable if you're using a compiled language, while network calls can be done with nothing more than curl/grpcurl/etc.
Define "easy" because curl is not "easy" compared to a function call and involves building a few large datastructures to work at all.
I think this is a blanket statement on my part, so to counter it; networked databases are definitely important. Some micro services maybe necessary for things outside of your control (HIPAA, Finance audits) and geo-proximity processing needs. And sometimes good ol micro services became popular because it was easy business in a giant org chart (Amazon, every team has an API).
This is a great approach. I like to add “macroservices” into conversations around this because I think it sums up the end state better. It’s not micro. It’s an entire billing system. It just only has API’s and not a front-end. Integrate it. That’s a macroservice. Or a service that handles the subscription, billing, and entitlements as one. That’s a macroservice and that’s totally acceptable.
What kills me is when there’s a monolith that breaks when other teams commit code. If I have to stash changes and deal with merge conflicts outside my team, it needs to be broken up.
Worry about scaling when you have a need to scale.
Macroservices are the way to go. Currently I work on a system that is a macroservice basically. We have more than one team working on the service which is great, since that means on-call rotations, upgrades, etc are much more reasonable. Having 50 people on a service really takes work off compared to 6, and there is a lot less reinventing the wheel. As long as the CICD pipeline is solid there really isn't any issues with a lot of teams working on the same code base.
As you say having a "Billing" service is much better than what you would typically see in a microservices architecture that you'll have a Billing-Stripe, Billing-Visa, Billing-Invoices, and so on, creating a web of dependencies.
Exactly. So long as merge conflicts stay within the team (i.e. your macroservice is logically divided within the code, models and services aren't just thrown into a models and services package or folder. I really detest merge conflicts when I can't track the work or have to reach out to another team to discern intent.
Having microservices for billing where you have Billing-stripe, Billing-visa, would make me want to scream. Why not a generic abstract or interface and those are simply implementations within the billing service? Macroservices FTW.
The important advantage is that you can scale parts of the "macroservice" independently when they're microservices. You can isolate stateful stuff into one microservice and then it is much easier to scale the other microservices horizontally.
I do this all the time. I call it Service Oriented Monolith. I.e. you use the principle of micro-services in a single code base. It works pretty well because micro-services gives you clear segregation.
Not the person you replied to, but I do the same thing and by default no.
So the definition of what a service is shrinks down really to a deployment artefact; each "service" is deployed individually as its own process (container). All shared data is persisted via a single REST-style API service. We nearly went all the way and deployed PostgREST for this but some specifics around security and a few other aspects stopped us. But in spirit, that is how it is architected : data persistence is a service that all the other "services" use.
The nice thing about this is you do still get a lot of the upsides. Because deployment is decoupled, you don't to have everybody constrained to the same development cycle. Service A can stay pinned at a specific version while service B is advanced to hotfix a critical change. Meanwhile Service C isn't blocked from deployment because Service A's tests are still failing. And we don't have to (necessarily) roll back Service D if prod deployment of Service E failed. etc etc.
I wouldn't. I would group them in logical groups, with the principle of moving complexity closer to where the data is.
A "Users" service might have crud operations, and maybe a few report generators that can run SQL against the Users database. But the "Users" services should not have access to "Account Balance" service, e.g. Then say, the KYC service can access the Users and the Account Balance services and then do a manual join on the data.
But say if KYC is directly accessing the Users database and the User table schema changes drastically, not only do you have to change the Users services, but you have to change KYC and coordinate KYC to deploy their changes at the same time your database changes.
The big problem is if the coupling is too tight, then it's hard to refactor in the future. OTOH if the coupling is too loose, then you might be making lots of overhead in computing when you could drastically simplify it by a tighter coupling of code.
The right answer I believe is to move the complexity close to where the data is. So maybe there is some complex KYC calculation on the Users service that is trivially solved by a custom SQL statement on the Users table. In this case it should probably be in the Users service even if it's only used by KYC, if this makes sense.
To put it differently, logical separation of state doesn't have to mean physical separation of state. One Postgres instance can hold your whole app's state -- just, put each service's data into a separate database within Postgres. I've done this and I like it a lot.
Well, I don't think you can have transactions open across logical databases any more than you can join across them. And you might anyway design the relevant component to serve multiple clients simultaneously, so you could still be executing multiple simultaneous transactions.
The component is the sole owner of its state. How it interacts with that state is an internal concern.
See also, Boundaries[1]. Apologies, I still can’t find a transcript since I last linked to it. The general idea is to write most of your code as functional and isolate small parts to handle state. As relevant here, by doing that you can trivially break out functions into services wherever you find it advantageous to do so.
Try to build your app as if you're going to reuse as much as possible in another app. This will make you think about clear boundaries and keeping things as standalone as possible.
This is a case of applying Phillipe Kruchten's 4+1 Views of Architecture slightly differently, such that logical, development and process views are not tightly coupled to one another - and works very well!
I'm doing this for my next-gen search engine, but I also queue several requests into a single one with my own webservice. Is this a known technique in the industry?
My previous company did this and it worked well enough! We used deployment groups to control traffic and specialize the monolith to serve certain part of the service
That's the point. At my last job, the training for microservice design was training in Domain Driven Design with Bounded Contexts. My predecessors at my current job could have used that training, because what I got was a different microservice for every data type, along with another microservice to essentially perform `JOIN` operations across these other microservices... When in reality, all these different pieces of data are just supporting information for the main entity we actually care about, and modeling it as a singular aggregate works much better.
Comments
My crazy advice: Build a monolith as microservices.
By which I mean focus on Domain Driven Development, focus strongly on modularity even when, and this is the key part, it's not required because it's in a monolith.
There are a great deal of advantages to microservices like teams being able to focus on a subset that can still be accomplished with a well designed monolith. While still avoiding the biggest headaches with microservices like managing a crazy network of deployments, across multiple environments where teams don't have the neccessary discipline to communicate.
If at a certain point scalability does become a big enough issue, then it's much more manageable to segment what's needed (and even transition back if needed). It still might take months, but the quality of the transition is much higher.
Here, here! If I had a dollar for every team I’ve seen adopt micro services as a solution to code modularization I’d have several dollars.
“Hey our code could be cleaner and our git repo seems to be getting big”
“Sweet let’s inject the network in between everything”
Having lived between packaged based repos (Amazon) and mono repo (FB), I've come to greatly appreciate the mono repo life. All my dependencies match up, and I can build everything in one command. Furthermore, I can run all 4007 tests in 13 minutes.
Agreed! I worked at a job where most of the company worked on a monolith, with the frontend in its own repo, then another 25 microservices hanging off the monolith. Everything was different in every repo, including how to deploy and run tests etc. The project I was working on was everything in one repo with a nice deploy.sh and test.sh and no chasing people to find out how to deploy that one microservice that was last touched months ago.
Cloud provider's best trick was to convince everyone that network calls are better than function calls.
Not really. Cloud providers did convince everyone that if you hit a resource limit in one of your boxes then your best strategy is being able to add more boxes, and not move everything and the kitchen sink to a larger box.
Also, not everything sits in the hot path of anything. Sometimes all you want is a long-running job to run somewhere.
I think they are, though. Off the top of my head:
* Network calls are traceable by default — logging all incoming queries is easy, logging all function calls is harder.
* Network calls are easily made — function calls in a backend can require passing some kind of a huge config structure, or might be entirely unavailable if you're using a compiled language, while network calls can be done with nothing more than curl/grpcurl/etc.
The downsides are a) decreased performance b) you have to handle failures c) dev ergonomics, but eh. I would still choose network calls over function calls for a big backend, probably.
It's really not though. Many (most?) languages have some way to annotate function calls with logging logic
Not really sure what you're referring to here
Oof. Everything in my 12-13 years of engineering has led me to the exact opposite conclusion of literally every point you made here.
Are you genuinely suggesting that making a network call is easier than calling a function in the same code base running in the same process?
Completely disagree if you work on low latency systems, or a high volumes of requests: functions are traceable if you need to in a crisis: plug a profiler, run like prod, see the hot paths.
Dev ergonomics explode the cost of everything and you spend your time trying to figure out what the hell is going on where rather than just... plugging the profiler and sending the input, then observe the internals moving around.
If the system can work without micro services, it should.
Define "easy" because curl is not "easy" compared to a function call and involves building a few large datastructures to work at all.
I think this is a blanket statement on my part, so to counter it; networked databases are definitely important. Some micro services maybe necessary for things outside of your control (HIPAA, Finance audits) and geo-proximity processing needs. And sometimes good ol micro services became popular because it was easy business in a giant org chart (Amazon, every team has an API).
Real world is messy.
I’m not anti micro services. I’m anti micro services as a code organization strategy.
There are very real reasons to need process isolation in larger systems. Code modularity is not one of them.
This is a great approach. I like to add “macroservices” into conversations around this because I think it sums up the end state better. It’s not micro. It’s an entire billing system. It just only has API’s and not a front-end. Integrate it. That’s a macroservice. Or a service that handles the subscription, billing, and entitlements as one. That’s a macroservice and that’s totally acceptable.
What kills me is when there’s a monolith that breaks when other teams commit code. If I have to stash changes and deal with merge conflicts outside my team, it needs to be broken up.
Worry about scaling when you have a need to scale.
Macroservices are the way to go. Currently I work on a system that is a macroservice basically. We have more than one team working on the service which is great, since that means on-call rotations, upgrades, etc are much more reasonable. Having 50 people on a service really takes work off compared to 6, and there is a lot less reinventing the wheel. As long as the CICD pipeline is solid there really isn't any issues with a lot of teams working on the same code base.
As you say having a "Billing" service is much better than what you would typically see in a microservices architecture that you'll have a Billing-Stripe, Billing-Visa, Billing-Invoices, and so on, creating a web of dependencies.
Exactly. So long as merge conflicts stay within the team (i.e. your macroservice is logically divided within the code, models and services aren't just thrown into a models and services package or folder. I really detest merge conflicts when I can't track the work or have to reach out to another team to discern intent.
Having microservices for billing where you have Billing-stripe, Billing-visa, would make me want to scream. Why not a generic abstract or interface and those are simply implementations within the billing service? Macroservices FTW.
The important advantage is that you can scale parts of the "macroservice" independently when they're microservices. You can isolate stateful stuff into one microservice and then it is much easier to scale the other microservices horizontally.
You might find the concept of Self-contained Systems, or SCS, interesting:
https://scs-architecture.org/
Disclaimer: I helped with the concept a tiny bit.
It sounds like you invested some effort to come up with a brand new buzzword to refer to microservices.
Sounds like microservices to me
I might start calling this a Megalith, since it kinda fits if you squint at the definition.
I do this all the time. I call it Service Oriented Monolith. I.e. you use the principle of micro-services in a single code base. It works pretty well because micro-services gives you clear segregation.
Does each module get its own separate data store? (Genuine question as I see some micro services with direct access to shared data.)
Not the person you replied to, but I do the same thing and by default no.
So the definition of what a service is shrinks down really to a deployment artefact; each "service" is deployed individually as its own process (container). All shared data is persisted via a single REST-style API service. We nearly went all the way and deployed PostgREST for this but some specifics around security and a few other aspects stopped us. But in spirit, that is how it is architected : data persistence is a service that all the other "services" use.
The nice thing about this is you do still get a lot of the upsides. Because deployment is decoupled, you don't to have everybody constrained to the same development cycle. Service A can stay pinned at a specific version while service B is advanced to hotfix a critical change. Meanwhile Service C isn't blocked from deployment because Service A's tests are still failing. And we don't have to (necessarily) roll back Service D if prod deployment of Service E failed. etc etc.
I wouldn't. I would group them in logical groups, with the principle of moving complexity closer to where the data is.
A "Users" service might have crud operations, and maybe a few report generators that can run SQL against the Users database. But the "Users" services should not have access to "Account Balance" service, e.g. Then say, the KYC service can access the Users and the Account Balance services and then do a manual join on the data.
But say if KYC is directly accessing the Users database and the User table schema changes drastically, not only do you have to change the Users services, but you have to change KYC and coordinate KYC to deploy their changes at the same time your database changes.
The big problem is if the coupling is too tight, then it's hard to refactor in the future. OTOH if the coupling is too loose, then you might be making lots of overhead in computing when you could drastically simplify it by a tighter coupling of code.
The right answer I believe is to move the complexity close to where the data is. So maybe there is some complex KYC calculation on the Users service that is trivially solved by a custom SQL statement on the Users table. In this case it should probably be in the Users service even if it's only used by KYC, if this makes sense.
This is similar to the premise of "Righting software" and volatility based decomposition as opposed to functional decomposition.
Your example is functional but you wouldnt have to change much to have it fit in the "engines and managers" pattern the author is fond of.
Yes. But in a single database instance. The Service Oriented Monolith also applies to the database.
To put it differently, logical separation of state doesn't have to mean physical separation of state. One Postgres instance can hold your whole app's state -- just, put each service's data into a separate database within Postgres. I've done this and I like it a lot.
Guessing you don't need atomic DB transactions in that case.
Well, I don't think you can have transactions open across logical databases any more than you can join across them. And you might anyway design the relevant component to serve multiple clients simultaneously, so you could still be executing multiple simultaneous transactions.
The component is the sole owner of its state. How it interacts with that state is an internal concern.
See also, Boundaries[1]. Apologies, I still can’t find a transcript since I last linked to it. The general idea is to write most of your code as functional and isolate small parts to handle state. As relevant here, by doing that you can trivially break out functions into services wherever you find it advantageous to do so.
1: https://www.destroyallsoftware.com/talks/boundaries
Try to build your app as if you're going to reuse as much as possible in another app. This will make you think about clear boundaries and keeping things as standalone as possible.
This is a case of applying Phillipe Kruchten's 4+1 Views of Architecture slightly differently, such that logical, development and process views are not tightly coupled to one another - and works very well!
There's already a term for it, the "modular monolith".
I'm doing this for my next-gen search engine, but I also queue several requests into a single one with my own webservice. Is this a known technique in the industry?
My previous company did this and it worked well enough! We used deployment groups to control traffic and specialize the monolith to serve certain part of the service
Just me, or does the definition of “Bounded Context” read similarly to typical recommended architectural boundaries for microservices?
That's the point. At my last job, the training for microservice design was training in Domain Driven Design with Bounded Contexts. My predecessors at my current job could have used that training, because what I got was a different microservice for every data type, along with another microservice to essentially perform `JOIN` operations across these other microservices... When in reality, all these different pieces of data are just supporting information for the main entity we actually care about, and modeling it as a singular aggregate works much better.
Microservices is the current mass yak shave exercise.