Don't be Afraid of the Monolith
19 Sep 2025
A common mistake I’ve seen is developers over-engineering new applications. While it can be partially caused by “ resume-driven design”, I feel it mostly comes from the assumption that the latest techniques seen online from large tech companies apply to everyone. Sure, the Googles and Facebooks of the world can be a great technical role model, but not every application has to be built like Netflix. I would even argue that a “next Netflix” shouldn’t be built initially like Netflix.
A city of 1,000 wouldn’t start with building airports, trains and highways even if it will become a city of 1 million. The same applies to software. Sure, consider the future of maximum scale, but new requirements and technologies will foil even the best plans. You just need to leave a way to evolve to the next stage.
A design pattern I see commonly abused in new projects is picking microservices over modular monolith. It’s hard to imagine a new application that should start its life as microservices. Even with a skilled team, the pressure to deliver the minimum viable product is strong enough such that there is a high risk of getting the worst of both worlds: a distributed monolith, or at best an over-designed and complex to debug system requiring dedicated DevOps engineers before you need them. In the beginning you should focus more on business requirements and value. I’ve seen this push to over-engineer many times – once, I was presented with a proposal to create a Kubernetes cluster for an application that otherwise ran with very little utilization on a single server with 1 vCPU and 4 GB memory.
Some rules of thumb that I have:
- If you have more services than team members, you’ve almost certainly done something wrong. A better ideal is one service per team (5-10 cross-functional people), even if that means starting as a monolith.
- Defer adding complexity to a system, especially if you can easily add it later.
- Don’t defer good design. Good design means clean division of responsibility (modularity) but without unnecessary abstraction that could be added later, allowing the design to evolve as it grows.
- A single server can handle far more than you think, a database even more so. Some internal applications will never need horizontal scaling.
- Your database can do a lot more than you think – you don’t need Elasticsearch and specialized vector databases for your small HR RAG chatbot of 100 documents when your SQL database (like Azure SQL or Postgres) already supports vector similarity search and full text search that work fine on datasets probably larger than you’d expect.
I recently came across an article “How to Scale an App up to 10 Million Users on Azure” by Dr Milan Milanović which is a great high-level software architecture discussion on an application’s lifecycle as it grows to global scale. The best part it reinforces: don’t be afraid to start small and simple. One of the essential tenets from this article is that you should scale architecture when a real pain point exists and not before.
Milan recommends starting with a modular modulith, and I agree. Modular monolith was called “good software design” prior to microservices, but as the term monolith got unconditionally disparaged during the microservices era, modular monolith differs from the poorly-designed “traditional” monolith of highly-dependent spaghetti code. Some developers assumed the then-new techniques like containers, CI/CD, canary testing and frameworks like Spring Boot came only in a microservices architecture; however, these great techniques still work in the monolith. A “modular monolith” enforces keeping modules small and to a single responsibility yet the application provides ways to scale with growth. Some frameworks have a way to enforce modular monoliths such as Spring Modulith. If you think the microservices pattern enforces good design, then consider doing the same with module boundaries instead of network boundaries and eliminate the complexities until requirements truly demand microservices, at which point turn the modules into microservices.
The whole article is worth a read and I agree with almost all of it. Here are my thoughts on it:
- I love the quote that “StackOverflow ran on a single database server until it had 10 million monthly users”.
- The introduction of Azure Front Door seems too early. However, Application Gateway can be useful in the single instance scenario for web application firewall or private virtual networks.
- It’s a simplified high-level view. Real applications might have some Azure Functions, storage services, data factory pipelines, DDoS protection and other elements even at the lowest user levels, so keep that in mind.
- Disaster recover is not mentioned. Even if you are in a single server, make sure your data backups are multi-zonal or multi-region. You might be OK not having a DR environment, especially if you can create it quickly with Terraform or an ARM template.
- I love the early addition of caching (Redis in Milan’s design). Caching is almost always a great step in performance after the basics like proper database indexes.
- The main thing keeping standard SQL from scaling is the enforcement of highly consistent transactional writes. The article eventually does mention brining in another database like CosmosDB for less transactional needs. I’d probably consider that before getting too crazy with hyperscaling or partitioning in Azure SQL.
Overall, this is a great article explaining how an application can start from a humble beginning and grow to maximum scale. Hopefully our posts give you the encouragement not to over-design early, but understand enough about the process to leave an avenue for continuous improvement.
More reading by the same author: