Monolith or Microservices?
Designing a system built for the needs of a startup, optimized for simplicity
In my time running engineering teams and advising startup founders on their own teams, I commonly encounter the monolith vs microservices debate. Usually, my knee-jerk reaction is to strongly dissuade them from switching to microservices, or if they’re already caught in its throes, give them even more reasons to consolidate and simplify.
Obviously, I’m biased, but it’s a natural outcome based on my experience. As an early engineer at Uber, I watched the business scale to dizzying heights, which for many justified the equally dizzying proliferation of microservices. Around early 2016, I recall a conversation with an infra leader who frustratedly pointed out that the number of services (5,000+ at the time) had finally exceeded the number of engineers. That’s 5,000+ separate data stores, on-call rotations, and deployment pipelines; some of which only had part-time support of a single engineer. Ironically, the top 10 services at the time consumed over 95% of the resources–I know because my team owned one of them–so the rest must’ve been a whole lot of fluff.
On the flip side, Stripe’s story, which I often draw as a counter example, shows how far you can take a monolithic architecture: 3,000+ engineers and $1T+ in payment volume. Of course, due to their scale they eventually broke out some key pieces into dedicated services, but by and large they definitely leaned towards monolith/macroservices rather than microservices. Meta, another household name with a world class engineering team, has a similar monolith story with their ginormous binary.
However, the takeaway isn’t that you should take a dogmatic stance and blindly do whatever you can to squeeze every ounce of juice out of your monolith; it’s that the decision to go with microservices, especially for small, inexperienced startup teams, is prematurely optimizing for problems that may never materialize, not to mention taking on undue burden despite much more pressing priorities. You’re much more likely to run out of money and die than to ever actually need microservices.
While the debate continues, the landscape has shifted beneath our feet. Languages, frameworks, and technologies have evolved dramatically and what we’ve classically known as monoliths and microservices are no longer the same. Ruby (Stripe) and PHP (Facebook) have lost favor with developers. Mobile has exploded and web clients are much richer, shifting rendering out of the back-end. Platforms have evolved with new infra and serverless offerings opening up new possibilities. Finally, AI is here and has up-ended how we build and architect our apps entirely. Monoliths have gotten smarter and service-oriented architecture (SOA) can be done in a much less scary way.
So, my stance on this debate is nuanced but firm: The choice between monolith and microservices is a false dichotomy. To find the right answer, you must zoom out and solve for the higher-order problems of the entire business, and consider how new realities like AI demand a fresh perspective. Therefore, I offer a playbook grounded on a philosophy of pragmatic simplicity, specifically for the founder at the whiteboard, ready to build a company that can achieve product market fit (PMF) and ultimately lasts.
Philosophy: Ruthless Simplicity
First things first, I’ll state front that I will not be entirely “objective”; this will be unapologetically opinionated. That’s because any two engineers can logically argue the merits of one approach versus another and end up both being “right,” a classic and oft-repeated endless/pointless debate. However, the right decision isn’t purely technical but rather a reflection of the team culture and underlying philosophy. So, I’ll start by laying down my philosophy, which I call Ruthless Simplicity™, a mindset that prioritizes simplicity and hates complexity.
In all of the engineering teams I’ve seen, there’s one constant that holds true: engineering velocity is the lifeblood of any startup. The ability to ship high-quality products, respond to market feedback, and iterate quickly to find PMF is what separates great teams from the rest. And the single greatest killer of velocity, again and again, is complexity.
Complexity is a tax on every single action your team takes. It’s a tax you pay when you try to hire or onboard a new engineer, when you try to debug a P0 incident at 4am, when you try to ship a new feature, or when you try to figure out which line items in your exploding infra bill can be cut. I’ve seen this happen repeatedly at every organization I've led or advised. Once complexity takes root, whether in your architecture, your tech stack, or your processes, it’s incredibly difficult and expensive to unwind. Especially when said unwinding requires a personnel change because the team was built around this complexity (sadly, I’ve had to deal with this).
Each of these moments is a small injection of friction, a tiny loss of momentum. Individually, they seem minor. Collectively, they are devastating. They create a constant drag that opposes every forward movement. The ultimate strategic advantage of simplicity, therefore, is not a single feature or capability—it is compounding momentum. It is the powerful, virtuous cycle created when hundreds of small, daily efficiencies add up to a massive, sustainable advantage over time, so long as you continually, and ruthlessly, eradicate complexity. As Elon Musk famously demands, “delete, delete, delete.”
Imagine two startups, both with a team of reasonably talented engineers, both tasked with building the same product.
Team Complexity chose a microservices architecture from day one. Their on-call engineer spends Sunday night correlating logs from five services to debug an issue. Their new hire spends their first week just trying to get all the services (and their databases) running on their machine before shipping a single line of code. A developer wanting to add a new feature has to coordinate with another team to get a corresponding API change deployed. The PM has to run separate queries across all the databases and manually join them in a spreadsheet. Everything is complex.
Team Simplicity chose a monolith. Their on-call engineer feeds Cursor a single log, has it scan a single repository, and finds the root cause in minutes. Their new hire gets the single service running in their first hour on the job, uses Cursor to gain context on the whole repo, and ships fully tested code to production that afternoon. A developer adding a feature can leverage existing components in the repo with no coordination and ship their change in a single pull request. The PM can answer all the critical business questions by querying a single database. Everything is simple.
After one year and a doubling of the team, the difference is not linear; it's exponential. Team Simplicity increased their velocity by continuing to invest in their shared environment and building up even more momentum. They have out-shipped, out-learned, and out-maneuvered their competitor, not because their engineers are necessarily better, but because their architectural philosophy freed them from paying the daily tax of complexity. The other team has quadrupled the number of services, spent more time battling their infra than finding PMF, and still can’t get clear answers on their business (because they’re still wrestling with their data warehouse).
This is why ruthless simplicity is so important. Without it, things go awry so easily at the whims of just a few misguided engineers. I think it boils down to a few guiding principles:
Less Is More. This is the central mantra. Every time you add a new language, a new codebase, a new database, or a new piece of infrastructure, you’re creating another source of friction; another thing to learn, remember, maintain, or be on-call for. Find ways to better utilize what you already have instead of adding new things to the mix. By embracing "less," you remove these taxes by default, making everything easier and contributing to your compounding advantage. The more you delete, the better.
Simplest Answer Wins. Empower your team with a simple heuristic: all else being equal, choose the simpler solution. A culture that rewards simple, effective solutions over complex, "intellectually interesting" ones is a culture that builds momentum. At times, teams will swoon over some sexy new tech, or be inclined to chase premature optimizations. Combat this by challenging engineers to simplify and then openly recognizing and rewarding those that do.
Compounding. When everyone is working in the same place, knowledge proliferates quickly and easily. Any time an improvement is made, it improves everyone’s lives. On-calls are now easier with a larger rotation, debugging is easier with more things centralized. Investments in this core place everyone works in will have high fanout, which ultimately draws more and more investment, which then will compound over time. It becomes your engineering velocity flywheel.
Smart Exceptions. There will always be valid exceptions, but apply rigor any time someone believes they need to be special. Do the gains of going off the blessed path outweigh the costs incurred? Can the existing systems be augmented or improved to support more use-cases? Maybe the monolith needs an experimentation module to serve as a playground before graduating to production (or easily retiring on failure)? Make room for this but ensure things are eventually consistent towards a simple picture.
With a full understanding of where I’m coming from you should now be able to more clearly understand if my recommendations are right for your team.
Working Backward
I’ve found the best way to arrive at great solutions is to start with the end outcome you want and then work backward. When it comes to architecture decisions, particularly foundational ones like monolith vs microservices, solutions can end up shortsighted often because they’re not looking at the problem from the right elevation. You should be wearing the CTO, or technical co-founder, hat in this case, but not one who is purely a technologist and rather one who understands the business and the realities of startups.
That person should be thinking:
It should be easy to build a team. Building a team starts with hiring, one of the hardest challenges for a startup, most of which have little funding and non-existent brand. Every complex, exotic choice you make about your architecture translates into a hiring hurdle. In this challenging talent market, the last thing you want to do is limit who you can hire. Furthermore, avoid the trap of trying to attract engineers who are enamored by sexy technology; they’re likely thinking of their own resumes rather than making your company successful. Design a system that is braindead easy for any capable engineer to work in so you can literally hire anybody who knows how to code but may not necessarily be experts in infra or distributed systems (hint: most aren’t).
It should be easy to build new features, fast. This is all about developer experience (DX). All new hires should be able to get their environment up and ship their first PR on day one. And I’m not talking about a copy change; it should be a real bug fix or small feature from your roadmap. For existing engineers, it should likewise be easy to build things because they’re only limited by their speed of coding, not any infra or coordination overhead. No need to learn new languages or infra, everything is centralized and consistent, code is often reused. Sounds daunting, but force yourself to think about what kind of system you’d need to have to make this possible. It just takes some creative thinking and a commitment to eradicating complexity.
It should be easy (and cheap) to keep the lights on. The more complex a system, the harder it is to maintain and debug. Less moving parts means less things to monitor, less pipelines to manage, less boiler plate to duplicate, less tools needed to manage the complexity. Less things also means less costs. Design a system that is just simpler in every aspect and you will have so much more room to breathe and focus on what you actually want to do, which is to build. Aim to have a team that spends less than 1% of their time simply keeping the lights on. Design a system simple enough that even you, the CTO, can be the sole SRE.
It should be easy to understand business health. Often overlooked in startups, business intelligence (BI) is critical in your journey to PMF. If you don’t know how you’re doing–how customers are using your product or not–then you’ll never be able to iterate towards the winning solution. Sure, you can leverage external platforms like product analytics, but nothing beats the ground truth stored in your database. Data engineering is not easy; many underestimate the difficulty in aggregating and maintaining all of a company’s data in a way that’s easy to report business KPIs. Design a system that dramatically reduces the data burden to the point where you don’t need to hire a dedicated Data Engineer.
Now that we have a clear picture of where we want to end up, let’s dissect monoliths vs microservices to arrive at our solution.
A Simple, Hybrid Approach
In the spirit of ruthless simplicity, our architectural debate has a clear starting point and a clear path forward. Instead of choosing between two competing ideologies, we’ll simply select the simplest possible path by default and then evolve it intelligently, addressing its key challenges one by one.
We anchor on the monolith for one overriding reason: it is the simplest possible way to build, deploy, and operate software. But beyond simplicity, what else does it give us? By design, we get unification, which is the core benefit from which all other benefits emerge: broad knowledge sharing, faster development, easier debugging, simplified BI, lower operational overhead, and more.
Critiques of this approach are well-known. We've all heard stories of, and seen for ourselves, monoliths decaying over time, becoming a big janky mess that is impossible to scale and terrifying to deploy. This is a valid fear, but it confuses the pattern with its worst-case implementation. The monolith is not the problem; it’s a lack of discipline. If you’re the type of team to let this happen to a monolith, switching to microservices does not actually make the problem go away, it only gets distributed and in a much worse way: janky microservices on janky infra with a janky DX.
So, there are real challenges with monoliths. Let’s worth through them one-by-one to devise the best solution anchored on simplicity.
Problem: Code Grows Unreadable
With more and more cooks in the kitchen, even a single codebase can devolve into an unreadable mess as developers bring their own styles and quirks to the table.
Right off the bat, it’s essential to create standards to ensure consistency. All engineers have to agree on styles, conventions, and ground rules. It doesn’t matter which–there’s no single right answer–it only matters that people all agree and stick to it. The most successful and scalable approach is to automate as much as possible into Git hooks/actions. PRs should be automatically flagged any time there’s a violation. Where lint rules fall short, you can develop PR agents that can assess code based on a STYLE.md
file in the repo. And when things slip past automation, then you fall back to humans who can uphold the standards. This is a valid strategy even for microservices.
Problem: Tight Coupling
As monoliths grow, dependencies compound, features get tangled, and code becomes difficult to refactor.
Modularity and isolation is a prime reason why engineers go with microservices, but it’s not hard to incorporate into a monolith, which has been popularized in the form of the modular monolith. You can start by splitting your code into a core layer, which contains primitives that almost everyone needs, and a modules layer, which contains all the “apps” built on top of the primitives. The core challenge here is to decide what is or isn’t shared.
Consistent code is already enforced by our approach to standards above, but beyond that I would enforce full decoupling of modules from each other, both at the database level and the interface level. While module tables can all live in the same database, you would want to prevent joining tables across modules. Similarly, I would recommend enforcing module interfaces through a service-like layer in the code, encapsulating everything behind a “public” service interface. In this way, developers can move quickly in their own lane if needed, but still benefit from a unified codebase.
Problem: "All-or-Nothing" Deployments
A single, monolithic deployment pipeline creates immense pressure. A one-line change requires a risky deployment of the entire application, causing teams to become risk-averse and ship less frequently, which frustrates the business.
De-risk releases by embracing feature flags and leveraging modern CI/CD. First, put new features and major changes behind a feature flag to decouple deployment from release. This enables safe dark launches (deploying code that is off by default) and gradual user rollouts, all controlled by an instant "kill switch" that is completely separate from the deployment itself.
Second, build a smart CI/CD pipeline that pairs robust pre-deployment testing with a critical post-deploy health check. Most deployment systems will look for `/health` endpoint to validate a new instance before it receives live traffic, automatically halting and rolling back any bad deploy. This is where you can invest in more sophisticated checks without needing to dive into advanced deployment strategies.
Finally, when things slip through the cracks, ultimately you need to be able to roll back quickly, so invest in automating one-click rollbacks.
Problem: One-Size-Fits-All Stack
A critical business need arises that is a poor fit for the monolith's primary tech stack. Maybe you’ve chosen a TypeScript stack but there’s a Python-only model you need, or maybe there’s a performance-critical step in your flow where a Rust component would totally shine.
We knew there would be valid exceptions, so we’ve come prepared with a disciplined approach. Here, we can look to a governed, specialized service commonly defined as a sidecar service. This is not simply opening the floodgates to more microservices, but rather a specific implementation of a service that remains tightly coupled with the monolith while allowing freedom to operate in the necessary domain. Keeping this from devolving into a rat’s nest requires developing a pattern where a very specific task can be performed out-of-process from the monolith, but immediately relayed back. The key discipline is that this service is a leaf node; it is called by the monolith but never calls the monolith back, preventing any circular dependencies.
Problem: Organizational Scaling Bottleneck
Your engineering team grows large–e.g. beyond 50–and despite modularity, the sheer number of developers working in one repository begins to create coordination overhead and team friction.
I’m not fully convinced this is a real or truly urgent problem, or one that can’t be mitigated with discipline and thoughtful design, particularly when you’ve already modularized your service. Obviously, I would point to all the examples of teams who’ve successfully scaled well beyond 50 with a monolith. However, I’ll resist being dogmatic and concede that at some point you might outgrow a monolith. Since multiple services will be an inevitable reality–hopefully not before you’ve reached 100 engineers–I would approach this with setting thoughtful constraints in order to mitigate the frenzy.
First, I would enforce a ratio between the number of engineers vs the number of services. Ideally, this would be something like 10:1 (100 engineers for 10 services), but no lower than 2:1. Second, I would enforce that every service has a dedicated on-call rotation with a minimum of 5 engineers on it, which means a worst-case monthly pager duty per engineer. Finally, I would only allow true service-oriented architecture (SOA) once I have 1) enough infra engineers to own the whole process of provisioning, deployment, observability, and SRE and 2) enough data engineers to own the data ecosystem, ensuring everything reliably gets into the data lake and also reliably makes its way to the business customers. Of course, builders will be responsible for what they build, but going SOA requires central authorities to define best practices and hold them accountable.
A Recipe For Zero to One
This philosophy of ruthless simplicity and the hybrid architectural model provide a powerful framework for decision-making. But theory is only useful when applied to reality. The true test of a strategy is how it performs under the pressures of growth, competition, and technical debt.
Let's translate this philosophy into a concrete playbook with a specific and actionable recommendation for technical co-founders. So, if I were starting a company today, what stack would I choose?
My mandate is to achieve maximum velocity to find product-market fit before we run out of money and die. I have the rare opportunity to build a culture of simplicity from day one. My goal is to choose a stack that is simple, powerful, and allows for rapid iteration without incurring the "complexity tax."
This is what it would look like:
TypeScript: JS and TS are the most popular languages on the planet so the hiring pool is enormous. In addition, there’s a rapidly growing TS movement around AI libraries, platforms, and tooling, which makes it well-positioned for an AI-first team. Finally, any modern application should have a rich web client where React would be the ideal choice, so if you have to use TS anyway, then might as well go all the way and have a full-stack TS repo. If you need mobile, the transition to ReactNative is pretty straightforward. One language to rule them all. Ruthlessly simple yet easily scalable.
Supabase: Owning and managing your own database is unnecessary, and Supabase adds a ton of value—e.g. auth, migrations, replicas, cron, real-time–on top of just having a DB. There’s a small learning curve and you have to work within certain constraints, but I think this is a good thing. For example, I think leveraging RLS is a big positive, and having a limited query interface keeps things simple like we want.
CloudFlare Workers: On top of great networking services, Cloudflare’s dev platform has been growing really nicely. I would use their serverless Workers offering where you can deploy multiple “services” (front-end and back-end) from a single repo. The biggest win I think is having free global distribution thanks to Cloudflare’s infra but also lightweight operational load because of serverless (not to mention crazy low cost).
Modularization: CF Workers supports first class worker-to-worker communication through a simplified “RPC” interface that just looks like a regular function call at the code-level. Basically, they’ve made “service-to-service” communication trivial; fully typed, transparent serialization, private network. So, I would modularize the monolith by putting “modules” into separate workers (as if they were microservices, which ironically is true) and binding them to the main worker to get free worker RPC; tight coupling at the interface level but loosely coupled code-wise. For database-level decoupling, module tables should be namespaced and I’d simply disallow joins between module tables. In Supabase, this is easy: all you have to do is clip foreign keys.
CI/CD: Workers comes batteries-included with basic, but powerful, CI/CD. You can hook into Github merges and script out whatever checks you need on deploy. Where I’d invest is building an agentic evaluator where you can enforce consistency across the codebase. It’d be easy to add a custom Github Action to do this. The real challenge is arriving at a consensus for a good STYLES.md doc and getting all engineers to align.
BI: There’s only one DB so this is easy. Supabase also makes read replicas dead simple. You might need to eventually join against other data sources, but you can punt this decision as there are many options and using Supabase doesn’t paint you in a corner.
Observability: Again, this is easy because you have a monolith. CF gives you great observability out of the box, but you can also push logs to your favorite platform like DataDog or New Relic. Like BI, your choices are not limited and you can punt until you need the additional power.
So, that’s my starter stack. Easy to hire for, easy to build quickly, very low cost and overhead, no real scaling barriers, and plenty of optionality should things change. It’s also primed for an AI-first team (CF has embraced MCP for docs and automation).
Conclusion
The debate between monoliths and microservices has always been a false dichotomy. The real discussion should be about how to manage complexity. By embracing a philosophy of ruthless simplicity, you choose to build systems that create compounding momentum, empowering your team to deliver value to customers faster than the competition.
The recipe I laid out here—a disciplined, serverless, modular monolith designed for clarity, speed, and low overhead—is the direct, practical implementation of this philosophy. It’s a modern architecture for a modern reality.
Most importantly, it’s an architecture designed for the age of AI. A unified codebase with clear, logical boundaries provides the perfect, high-context environment for our new AI partners to learn, refactor, and build alongside us. The most effective architecture of tomorrow will be the one that is simplest not just for humans to understand, but for AI to master. For most of us starting that journey today, that architecture begins with the monolith.