← Back to list

Why We Split Our NestJS Monorepo into Services — Then Merged It Back

Viraj Lakshitha Bandara · 2026-04-13 23:49 · 0 claps · 8.7 min read paywalled
#nestjs #microservices #architecture #backend #production
Open on Medium ↗
Wiki topics: 🌐 · Web Development 🏛️ · Architecture

Why We Split Our NestJS Monorepo into Services — Then Merged It Back

It was 2 a.m. when the alerts started firing. Our payment processing pipeline had stalled. Users were completing checkouts, but orders weren’t being created. The culprit? A cascading failure across three NestJS microservices that were supposed to handle a single business transaction. The user service authenticated the request, the billing service charged the card, but the order service never received the event because our message queue had a transient network hiccup. We had retries, idempotency keys, saga orchestration — all the patterns the blog posts recommend. But none of that mattered when we discovered our distributed transaction had left money charged and no order in the database.

That night, sitting in a Slack war room with my tech lead, we asked ourselves the question that would reshape our entire architecture: why did we split this in the first place?

The Seduction of Service Boundaries

NestJS makes modularity feel effortless. The framework practically begs you to think in terms of clean boundaries: modules, providers, dependency injection that feels almost Spring-like in its elegance. When our startup began scaling from three engineers to twelve, the decision to split our monolithic NestJS application into microservices felt inevitable. We had read the Google SRE book. We had watched the Netflix and Uber tech talks. We knew the scripture: services should be small, focused, independently deployable.

Our reasoning was textbook sound. We had distinct domains: user management, billing, content processing (we were building an AI-driven content platform), notifications, and analytics. Each domain had its own data models, its own team ownership brewing on the horizon, its own scaling characteristics. Billing needed high consistency and audit trails. Content processing was CPU-bound and needed horizontal scaling. Analytics could tolerate eventual consistency. The boundaries seemed obvious.

So we did what the architecture astronauts told us to do. We carved up our monolith into five services, each with its own NestJS application, its own database, its own deployment pipeline. We built shared libraries for common types and DTOs. We introduced a message queue for async communication. We patted ourselves on the back for following best practices.

Three months later, our velocity had dropped by forty percent, and our infrastructure costs had nearly doubled.

The Hidden Tax of Distribution

The problem with microservices isn’t that they don’t work — it’s that they solve problems most mid-scale systems don’t actually have, while creating problems those systems absolutely cannot afford.

Our first wake-up call came from authentication. In the monolith, auth was a single guard decorator. Request comes in, JWT gets validated, user context gets attached to the request object. Done. In our brave new distributed world, every service needed to validate JWTs. We built a shared auth library, which seemed smart until we needed to add role-based permissions that queried the database. Now every service needed database access to the user service’s data, or we needed to make HTTP calls back to the user service for every request. We chose the latter, adding 50–150ms of latency to every single operation across the system.

Then came the orchestration nightmare. Our content processing workflow required coordination between four services: user validation, content upload to S3, AI processing job creation, and notification dispatch. In the monolith, this was a single controller method with a service layer calling other services. Synchronous, transactional, debuggable. In microservices land, it became a choreography of HTTP calls and message queue events.

We tried saga patterns. We introduced a workflow orchestrator. We built compensating transactions for rollback scenarios. Every solution added complexity, and none of them solved the fundamental issue: we had taken a workflow that belonged together and artificially separated it across network boundaries.

The worst part? Debugging. When something failed in the monolith, we had a stack trace. In the microservices world, we had distributed tracing with Jaeger, correlation IDs in every log line, and late-night spelunking through five different Kubernetes pods trying to reconstruct what actually happened. Our mean time to diagnosis tripled.

What the Blog Posts Don’t Tell You

Here’s what I learned the hard way: NestJS is excellent at creating modular monoliths. The framework gives you all the tools to build clean boundaries, dependency injection, isolated modules — everything you need to keep code organized without paying the distributed systems tax.

The microservices playbook you read about comes from companies running at scales you probably aren’t. When Uber talks about microservices, they’re managing hundreds of services, thousands of engineers, and deployment frequencies that demand independent release cycles. When Netflix architected around services, they were solving for regional failover and multi-datacenter redundancy. These are real problems, but they’re not your problems when you’re a team of fifteen engineers handling ten thousand requests per minute.

We fell into the trap of premature optimization — not for performance, but for organizational scale we hadn’t reached and might never reach. We designed for the team we wanted to be instead of the team we were.

The transaction boundary problem was particularly insidious. In a monolith, database transactions are straightforward. You open a transaction, do your work across multiple tables, commit or rollback. Atomic, consistent, isolated, durable — the ACID guarantees we’ve relied on for decades. The moment you split services with separate databases, you lose this. You’re in distributed transaction territory: two-phase commits, saga patterns, eventual consistency.

We burned two sprints building a saga orchestration system for order processing. It worked, technically. But it introduced edge cases we never had before. What happens when the billing service charges the card, then crashes before publishing the success event? What if the order service is deploying when the event arrives? We built retries, dead letter queues, idempotency checks, reconciliation jobs. We turned a fifty-line controller method into a state machine spanning three repositories and two thousand lines of infrastructure code.

The Path Back to Sanity

The decision to consolidate wasn’t easy. We had already committed. We had CI/CD pipelines for each service. We had separate databases. We had team members who had read “Building Microservices” and believed in the vision. Suggesting we merge back felt like admitting defeat.

But the data was undeniable. Our deployment pipeline took forty-five minutes to run the full test suite across all services. Our staging environment required spinning up five separate NestJS apps, five databases, a message queue, and a service mesh. New engineers took a week just to get the local environment running. Our cloud costs were approaching five figures monthly, mostly from running redundant infrastructure for services that could have been single-process threads.

We started with the most painful integration point: billing and orders. These two services communicated synchronously for every purchase. There was no independent scaling need — they scaled together. There was no team boundary — same three engineers worked on both. There was no deployment independence benefit — we always deployed them together anyway because changes in one required changes in the other.

We merged them back into a single NestJS application with two distinct modules. The code structure barely changed — NestJS modules provide excellent boundaries. But suddenly, our distributed saga became a database transaction. Our 500ms multi-service orchestration became a 50ms database commit. Our debugging experience went from distributed tracing archaeology to normal stack traces.

The win was immediate. Deployment time dropped by twenty minutes. Infrastructure costs decreased by thirty percent. Most importantly, we could move fast again.

We repeated this process with content processing and notifications. We kept analytics separate — it genuinely had different scaling characteristics and could tolerate async updates. We kept the user service separate initially, then eventually merged it too when we realized it was just adding latency with no real benefit.

The final architecture was a monorepo with a single deployable NestJS application, cleanly organized into domain modules. We kept the logical separation — billing code doesn’t import from content processing, enforced by linting rules. But we eliminated the physical separation that was killing our velocity.

The Microservices Cargo Cult

There’s a cargo cult in our industry around microservices. Engineers see successful companies using service-oriented architecture and assume that’s why they’re successful. The causality runs the other way. These companies succeeded despite the complexity of microservices, not because of it. They adopted microservices because they hit scaling problems — organizational or technical — that required that architecture.

The dirty secret is that most systems never reach that scale. Your startup handling a few hundred requests per second does not have the same problems as Amazon. Your team of twenty engineers does not need the organizational boundaries that make sense for a team of two thousand.

NestJS, in particular, is remarkably well-suited for modular monoliths. The dependency injection system, the module boundaries, the middleware pipeline — these give you all the structure you need to keep a large codebase organized without fragmenting it across network calls. You can still achieve clean architecture, domain separation, and testability. You just do it in-process instead of over HTTP.

One common misconception is that monoliths can’t scale. This is demonstrably false. Shopify runs on a Rails monolith handling massive traffic. Stack Overflow famously runs on a monolith. The secret is that vertical scaling and horizontal scaling of stateless application servers can get you shockingly far. A well-optimized monolith can handle tens of thousands of requests per second on modern hardware.

Another misconception is that microservices enable team independence. In theory, yes. In practice, when your domains are tightly coupled — and in most businesses, they are — you just move the coupling from in-process function calls to network calls. You haven’t eliminated the dependency; you’ve made it slower and more fragile.

Thinking About Architecture Differently

The lesson isn’t “never use microservices.” It’s “understand what problem you’re actually solving.”

Microservices are an organizational scaling pattern disguised as a technical architecture. They make sense when you have multiple teams that need to move independently, when you have domains with genuinely different scaling or availability requirements, or when you need to isolate failure domains in specific ways.

For most mid-scale systems, especially in the early years, a well-structured modular monolith will serve you better. You get faster development, simpler operations, easier debugging, lower costs, and better performance. You can always extract services later when you have concrete evidence that you need to.

The heuristic I use now: start with a monolith organized into clean modules. Extract a service only when you can clearly articulate the specific problem it solves and why solving that problem is worth the distributed systems complexity. “It’s best practice” is not a valid reason. “We might need to scale this differently someday” is not a valid reason. “This specific domain has a different availability requirement and we need to isolate its failures” is a valid reason.

For NestJS specifically, lean into the framework’s strengths. Use modules to create boundaries. Use dependency injection to manage dependencies. Use guards and interceptors to share cross-cutting concerns. You can build a beautifully organized, maintainable codebase without ever making a network call to yourself.

When you do need to extract something, be surgical. Don’t split along every domain boundary. Split the one piece that genuinely needs isolation. Keep the transaction boundaries intact. Don’t split read and write paths into different services unless you have a specific scaling need that requires it.

The Monolith We Rebuilt

Our current architecture is a single NestJS application deployed as multiple instances behind a load balancer. Inside, we have clean module boundaries: billing, content, users, analytics, notifications. These modules don’t import from each other’s internals — we enforce this with architectural linting. Shared types live in a common module.

We use database transactions for consistency where we need it. We use async jobs (via Bull queue, running in the same process) for background work. We use caching aggressively with Redis. We use feature flags for gradual rollouts. We monitor with the same observability tools we used for microservices — Datadog, structured logging, error tracking.

Our deployment pipeline runs in twelve minutes. Our staging environment is a single Docker container. New engineers can start contributing on day two. Our infrastructure costs are a third of what they were. Our p95 latency is under 100ms for most endpoints. We handle our current load with four application servers and have headroom to 10x before we’d need to rethink anything.

Most importantly, we’re shipping features again. The cognitive overhead of distributed systems was drowning us. Removing that burden let us focus on the actual product.

Would we do microservices again in the future? Probably, if we hit the right inflection points. If we scale to fifty engineers and need team autonomy. If one domain needs radically different scaling (say, our AI processing becomes 100x more resource-intensive). If we need to support multi-region deployments with local data residency. These are concrete, measurable problems that microservices solve.

But we won’t do it because it feels like what “real” engineering teams do. We won’t do it because a blog post from a company at 100x our scale recommends it. We’ll do it when the pain of the monolith exceeds the pain of distribution — and we’ll have metrics to prove it.

References

Martin Fowler — Monolith First

Shopify — Deconstructing the Monolith

NestJS — Application Architecture

Cindy Sridharan — Testing Microservices, the sane way

Segment — Goodbye Microservices

Sam Newman — Building Microservices (Book)


메타데이터
post_id
58cb51203077
slug
why-we-split-our-nestjs-monorepo-into-services-then-merged-it-back-58cb51203077
url
https://medium.com/@vitiya99/why-we-split-our-nestjs-monorepo-into-services-then-merged-it-back-58cb51203077
canonical_url
https://medium.com/@vitiya99/why-we-split-our-nestjs-monorepo-into-services-then-merged-it-back-58cb51203077
author_url
https://medium.com/@vitiya99
status
ok
fetched_at
2026-07-11 07:02:50