Back to Research
Architecture2025-12-03·6 min read read

Why We Stopped Using Microservices for SMB Projects

microservicesmonolitharchitecturesmb
Why We Stopped Using Microservices for SMB Projects

Between 2018 and 2022, we built every backend as microservices. It was the industry consensus: microservices were the professional way to build software. Monoliths were legacy. If you were not running Kubernetes, you were not serious. We had the Docker Compose files, the service mesh, the message queues, the distributed tracing. We were doing it right.

Except we were not. We were spending forty percent of our engineering time on infrastructure instead of features. We were debugging network issues between services instead of shipping product. We were maintaining eight separate deployments for applications that served three hundred users. We were, to put it bluntly, cosplaying as Google while building apps for local businesses.

The breaking point was a project for a logistics company. Fifteen employees. Needed a dispatch management system. We built it as five microservices: auth, dispatch, fleet, notifications, and reporting. Each had its own database. They communicated via RabbitMQ. Deployed on a small Kubernetes cluster. Total infrastructure cost: four hundred and twenty dollars per month. Total time to build: fourteen weeks.

Six months later, we built a nearly identical system for another logistics client. This time, as a modular monolith. Single Next.js application with a well-structured backend. Single PostgreSQL database with schema separation. Deployed on a sixty-dollar-per-month Railway instance. Total time to build: six weeks. The client could not tell the difference. The application performed identically. The only difference was that we shipped in less than half the time and the client paid a fraction of the hosting cost.

That was the moment we changed our default. For SMB projects -- which make up about eighty percent of our work -- we now build modular monoliths. We reserve microservices for the specific situations where they provide genuine value.

Let us define what we mean by "modular monolith." It is a single deployable application where the code is organized into well-defined modules with clear boundaries. Each module owns its domain logic and data access. Modules communicate through internal interfaces, not network calls. The critical point: the modular boundaries exist in code, not in infrastructure. You get the organizational benefits of service separation without the operational overhead of distributed systems.

Our standard structure looks like this: a single repository with top-level directories for each module (auth, dispatch, fleet, etc.). Each module has its own routes, services, repositories, and types. Modules expose a public API through an index file. Cross-module communication happens through function calls and shared interfaces, not HTTP requests or message queues. The database is shared but each module owns its tables and accesses other modules' data only through the public API.

The advantages for SMB projects are overwhelming. Deployment is a single process. There is no service discovery, no network latency between services, no distributed transaction management, no eventual consistency headaches. A database transaction can span multiple modules because it is all one process. Debugging is straightforward: you set a breakpoint and step through the code. No tracing correlation IDs across five services.

Development speed is dramatically faster. In our microservices projects, adding a feature that touched two services typically took three to five days because of the coordination overhead: updating the API contract, handling serialization, managing deployment order, testing the integration. In a modular monolith, the same feature takes one to two days because a cross-module feature is just a function call.

Here is when microservices still make sense. First, when different parts of the system have genuinely different scaling requirements. If your image processing service needs to scale to a hundred instances while your auth service sits at one, separate deployments make sense. But for an SMB app serving a few hundred users, every component has the same scaling requirement: one instance.

Second, when different parts of the system need different technology stacks. If your main application is TypeScript but your ML pipeline needs Python, separate services are justified. But this is a technology constraint, not an architectural choice.

Third, when you have multiple teams that need to deploy independently. This is the original motivation for microservices at Amazon and Netflix. If you have two hundred engineers and deployment coordination is your bottleneck, service boundaries that align with team boundaries solve a real problem. If you have a team of two to five people, deployment coordination is a Slack message that says "I am deploying."

Fourth, when you are building a platform that external developers will integrate with. A well-defined API service that third parties depend on should be independently deployable so you can version and scale it separately from your internal systems.

Notice that none of these apply to a typical SMB project: a custom application for a business with ten to five hundred employees, built by a small team, serving a predictable number of users.

The counter-argument we hear most often is "but what about when you need to scale?" This contains a hidden assumption: that a monolith cannot scale. A well-built monolith on modern infrastructure scales vertically to a surprising degree. A single Railway or Fly.io instance with four vCPUs and eight gigabytes of RAM can comfortably handle thousands of concurrent users for a typical business application. If you genuinely outgrow that, you can run multiple instances behind a load balancer. And if you truly outgrow that architecture, congratulations -- you have a successful business that can afford to invest in a microservices migration. That is the good kind of problem.

The migration path from modular monolith to microservices is well-understood and relatively straightforward, precisely because the module boundaries already exist. You extract a module into its own service, replace the internal function calls with API calls, and deploy it separately. The reverse path -- migrating from microservices to a monolith -- is much harder because you have to untangle distributed state and async communication patterns.

Our recommendation is simple. Start with a modular monolith. Invest the time you save on infrastructure into building features, improving UX, and shipping faster. If and when you hit a genuine scaling wall that a single process cannot solve, extract the specific module that needs independent scaling into its own service. You will make that decision with real production data instead of architectural speculation, and you will have a profitable business funding the migration.

About the Author

Fordel Studios

AI-native app development for startups and growing teams. 14+ years of experience shipping production software.

Want to discuss this further?

We love talking shop. If this article resonated, let's connect.

Start a Conversation

Ready to build
something real?

Tell us about your project. We'll give you honest feedback on scope, timeline, and whether we're the right fit.

Start a Conversation