Will you use a gun to kill a fly? The answer is NO. The same thing goes when you want to write a microservice application that is going to be maximum of 10K lines of code. Don't over-engineer it.
Ok, you have been writing and maintaining large code bases. Some might be 100K+ lines of code but you need to get over your habits and think differently. This post helps you unravel the mysteries of writing small and maintainable microservices. The suggestions are opinionated but it is in practice in a million dollar business.
TLDR;
Microservices are small software systems. You can safely ditch MVC. Say no to ORM and also not take the design pattern baggage for microservices. Focus on code performance, readability, and maintainability not some old rules and patterns. Those patterns were made when people were not doing microservices.
Why write microservices in the first place?
Microservices architecture, in my opinion, is breaking one or more monoliths into multiple smaller systems. These are more maintainable, independently developed and deployed pieces of software based on business functions. These smaller (presumably “micro”) systems should focus on only one business function and do it well. The catch here is "micro", these pieces should ideally be under 10K lines of code.
As they are independent it helps the business release features faster.
The shipments team is not dependent on the checkout team. Something deployed on the shipments app is never going to break checkout.
It becomes very decoupled. The blast radius of each change is controlled. That is the reason for rapid microservice adoption.
Now let's look at the ways you were used to doing things and why it makes less sense in this microservices era.
Do you need MVC?
Model-View-Controller, I got introduced to it in 2007 or maybe a bit earlier. Then I used to think it was the silver bullet to all software architecture issues. I don't hold that opinion anymore.
Yes, you used to work with Java or PHP and every other framework was MVC based. Now, you don't need to be strict about MVC anymore.
Focus on clarity and getting things done.
Use controllers if you want and if it still makes sense. Think like my app gets HTTP request and it has to give back HTTP response. Do think of having a backend API and frontend(s) consuming it. Check the code below, it is surely not MVC:
async function get(params) {
const today = new Date().toISOString().split('T')[0];
const {fromCurrency='AUD', toCurrency='USD', onDate=today} = params;
let exchangeRates = await db.query(
`SELECT rate, created_at FROM exchange_rates WHERE from_currency = ? AND to_currency = ? AND on_date = ?`,
[fromCurrency, toCurrency, onDate]
);
if (exchangeRates.length) {
const rate = Number(exchangeRates[0].rate);
console.log(`Found exchange rate of ${rate} for ${fromCurrency} to ${toCurrency} of ${onDate} in the db`);
return {fromCurrency, toCurrency, onDate, rate};
}
return getExternal(fromCurrency, toCurrency, onDate);
}
module.exports = {
get
}
You can see the full app here. Verify the structure it is not MVC :)
So rather than doing an effort to get precise lines of M-V-C, write tests, implement continuous integration. Add some logs and monitoring to the app. Make the code maintainable, keep it as lean and simple as possible.
Don't take on the ORM overhead
Object Relation Mapping (ORM) when I first saw an ORM in action, I said to myself this is one of the best things ever known to programmers. 10+ years later I would be cautious to suggest an ORM to any software engineer.
Last year I refactored a full ORM implementation to a raw SQL query way and it made that part of the application perform 20% faster.
On top of it, the database transactions were evident and the code was far more readable hence maintainable.
Data mapper or Active record both bring their own opinions, ways of doing things and extra weight. This not only causes performance issues but also code readability suffers. Think of the pre and post hooks/event listener Doctrine has, they work like magic and it is always tricky to understand magic.
Just try this, explain how an ORM related insert code works VS how a simple and straightforward INSERT SQL query works to a beginner/junior software engineer. You will already regret using that ORM. Especially in the context of microservices ORM is a clear overhead. The microservice is anticipated to be maximum of 10K lines of code and affect hardly 10 tables so just don't use an ORM, period.
Design patterns might be a baggage
I am not saying that you don't need to learn about software design patterns. You should know about SOLID, law of Demeter, factory pattern, strategy pattern, singleton, adapter pattern etc.
Well, most of these make sense if you do object-oriented programming right? What if you write a microservice in Node JS that is 1k lines of code spread across ~7 files.
It does one small slice of the business function. All these patterns become nice to know stuff at that point. Rethinking on it, all these patterns and rules were made with large code bases in mind and before microservices were adopted widely.
Design Patterns are relevant for a code base that is already big and in the next 6-12 months is going to be bigger, your usual monolith. They can turn out to be "extra baggage" for a service that is 100s of lines of code now and will become 1000s of lines of code in the next 6-12 months. We never foresee it to be bigger than that because to do that other part we will have another microservice. So keep your microservice code fat free and well tested.
Conclusion
If you still want to code your microservice like the last monolith you worked on maybe you are doing something wrong. Think of it again, if you go for a day trip you don't pack and carry things like you are going for a 2 weeks vacation. Think of code performance and maintainability, let the data speak for you and break the rules. Happy software engineering!
Originally published at geshan.com.np.
Top comments (14)
I agree with what you are saying. However, I've run into two issues with this when writing corporate in-house services in the .NET environment.
First, the repository pattern as done with Entity Framework has become so prevalent that any other solution or pattern is immediately regarded as "wrong". This means adding a considerable amount of ORM design and coding overhead where it may not be needed.
Second, you are rarely working with a totally new system. Instead, there's almost always a huge technical debt of legacy systems. Most of them have been built up over many years with feature after feature being added, sometimes in a haphazard fashion. This has led to tight coupling and a resistance to making changes because of what that refactoring would break.
I think you have already made it clear with "corporate in-house services" which are surely not microservices. And if you are not working with totally new systems are they even microservices? :)
Part of our goal is to reduce the current technical debt by moving legacy to a new microservice architecture. However, there is considerable legacy code that has to be kept running while this new work is being done. The challenge is to create the new service structure while keeping the old, which is everything from old VB6 desktop apps to tightly coupled WCF services, running. This creates considerable drag on our development process due to the fragility of these old systems and resistance to making changes. I think this is something anyone working in a environment that's been in production a long time is going to encounter.
Yes. Looks like you need find a new and better place to work may be :).
It's a great place to work. They are making a huge investment in upgrading technology. But there's a lot of work to be done and there are some challenges.
It’s indeed an interesting read! Just to offer a different take, I’d say that it really holds about the infrastructure, but that the business rules have to live somewhere. And even considering only one part of the business domain is in each micro service, it still should abide by good code design principles.
Because when it comes the time to “add just that new feature”, you’ll want to have your code flexible enough to sustain it, and not go make another service because, well, things are getting too messy. (I’m not saying that’s what you propose, though) That way you’d be just transferring code complexity to orchestration complexity, and an http mess...
So, if it’s an OO language, I think you should take just enough time to model your domain properly, put the necessary interfaces in place, use the appropriate design patterns, and then go do your next thing. A micromess is still a dangerous mess, and a pretty micro application is probably never going to bite back at you.
Does it make sense to you?
Thanks for the insightful reading!
Yes some parts you wrote surely makes sense. The thing about http level complexity tools like kuberernetes help a lot to handle it. Thanks for your views.
Thank you for the TLRD.
Speaking of the subject, I saw a talk recently
in which gave an example of monolith-services, which had only 1 instance and communicating sync with the other monoliths. It was a horror image, never thought that someone could achieve that :)
Also like the talk said, micro services were created to scale an organization, and less about the code (See Amazon 15yrs ago). If you have less then 100s developers the operational overhead of services will be a big % of your workforce, that if you do not use managed services like AppEngine and such.
There are multiple ways to look at it. In my previous work we were 10 backend devs and 2 devops/SRE people responsible for ~150 microservices running on k8s. At the end it is generally about code ;).
Sounds great, is there an article or talk about it somewhere? Sounds like a good win over a complex problem. Most of the stories I know had 10devs only for operational glue at that scale.
I can write a talk if this topic deserves more attention :) thanks!
Interesting read.
Thanks.
Lol blast radius!