At Metaview, we run interviews for a living. Notetaker, Application Review, Sourcing, Outreach, Reports. Each one is a different shape of workload, with a different scaling profile, owned by a different slice of the team. The backend has to keep all of them fast, reliable, and cheap as the company doubles in headcount and the codebase doubles in surface area.

That's a hard problem with two famously hard answers. Monolith and you eat the deployment, scaling, and reliability tax. Micro-services and you eat the orchestration, dependency, and migration tax. The literature treats it as a binary: start as a monolith, graduate to micro-services when the pain gets bad enough, then spend the next two years fighting the new pain.

We didn't want to do that. So we picked a third option: a serverless monolith. The structural simplicity of a monolith (one repo, one language, one database, shared libraries) with the deployment and scaling independence of micro-services (each workload deploys on its own cadence, scales to its own load). This post walks through how the architecture is laid out, why the trade-offs are sharper than the textbook framing, and the operational sharp edges we're already watching for.

The common journey

Across three companies in my career (VMware, Uber, and Docker) I've watched the same story play out. A product starts as a monolithic backend because that's the fastest way to ship the first version. Over time the costs of that choice compound, and the team gets pushed into micro-services. The reasons are always roughly the same four:

  • Cost. Infrastructure gets over-provisioned to support the widest workload the application has to serve. Every instance carries the headroom of the heaviest job.
  • Velocity. Deployment pipelines slow down and grow complex as the application gets bigger and more teams need to ship through the same gate.
  • Reliability. Single deployments start batching multiple unrelated changes together. Ownership of a regression becomes a forensic exercise.
  • Local development. The app becomes impossible to run on a laptop, and deploying it anywhere outside production is its own multi-day project.

The people who suffer are customers. The product gets less reliable, and improvements take longer to reach them. So teams break the monolith into micro-services along domain boundaries: one workload, one database, one deployment pipeline, one codebase per service.

And then the new pain shows up.

  • Infrastructure cost and complexity explode to support the growing number of independently deployed applications.
  • Reliability drops when services start depending on each other in ways nobody predicted, because domain boundaries are impossible to nail at the start.
  • Velocity decreases again as code sharing and multi-service changes get harder, especially if different services have picked different languages or frameworks.
  • Endless migrations start eating engineering time. Every service owner has to convince every consumer to adapt to every change they want to make.

So teams start reaching for monorepos, shared library versions across services, restrictions on language and framework choice. Things that were a given in the monolith world, now reintroduced one painful policy at a time.

Monolith architecture diagram showing workloads 1 to 5 deployed and scaled together
In a monolith, workloads 1 to 5 are deployed and scaled together even though they have different CPU, memory, and traffic requirements.
Micro-services architecture diagram with workloads scaled to their own needs
With micro-services, the different workloads are scaled according to their needs, but companies now worry about managing many storages, service discovery, orchestration, surface area, and implicit dependencies.

These internal migrations are large, costly, and (this is the worst part) invisible to customers. Engineers spend a year wrestling a system into a new shape that the people paying for it cannot see and would not pay for if they could.

The real coupling problem

I'd argue the underlying cause of this whole journey is one specific kind of coupling: infrastructure coupled to domain boundaries.

In the monolith world, the whole application is deployed as one piece. The assumption is that any change might affect the whole domain, so the infrastructure is provisioned for the whole domain. That assumption is wasteful, but it's at least internally consistent.

In the micro-services world, you split domains and infrastructure in a one-to-one fashion. That's clean in a frozen state. But the business doesn't freeze. Domain boundaries shift faster than infrastructural ones, because the business model changes faster than the way you provision compute. So mini-monoliths start forming inside the micro-services environment as related logic re-aggregates around real problems, and you end up running costly migrations to split them up again.

The interesting move, then, is not "monolith vs. micro-services." It's decoupling the infrastructure boundary from the domain boundary entirely, so the two can evolve at their own rates.

Why the dichotomy is misleading

Both monoliths and micro-services force a one-to-one mapping between domain and deployment unit. Monolith: one domain, one deployment unit. Micro-services: many domains, many deployment units. Neither lets you say "this workload should scale independently, but it's still part of the same business domain as that other workload, and they share the same database."

That last sentence describes most real systems most of the time.

Metaview's serverless monolith

At Metaview we lean heavily on the serverless paradigm. It comes with its own benefits and drawbacks (more on those below), but the structural property that matters is this: serverless lets you scale each workload independently, deploy changes in whatever grouping you like, and change that grouping cheaply. It collapses the coupling between infrastructure and domain.

The priority order we optimize for, in this order: customer experience, engineer productivity, systems reliability, infrastructure costs. Below is the structure we currently use.

Repo layout

All backend code lives in one monorepo, one language, with this simplified shape:

 - core/services/service_1, service_2,..., service_n
 - stacks/stack_1, stack_2,..., stack_n

Services represent business logic split into different domain units. Each service is a class with some business logic that can be used across stacks. Stacks are groups of serverless functions deployed together. They're usually grouped by workload type: serving web app traffic, ETL, event-based processing, certain types of third-party integrations.

External libraries are shared under core, with the exception of stack-specific ones (GraphQL being the obvious case). Every stack and every function can be run locally against staging or production dependencies. The OLTP database is shared across stacks.

What gets deployed when

Each stack deploys independently, and only if there are changes to it. So a change to the ETL stack does not redeploy the web-app traffic stack, even though both depend on the same core/services business logic. If the business logic itself changes, the stacks that depend on it redeploy; the ones that don't, don't.

This is the move. The deployment unit is the stack, not the domain. A stack can pull from any combination of core services. Stacks can be split, merged, or reshuffled cheaply, because they're just groupings of functions over shared libraries.

Serverless is much like wireless. Wireless doesn't mean there are no more wires involved, it's just that you don't need to worry about them most of the time.”
Metaview Engineering On the serverless monolith

What this actually gives us

This setup keeps a lot of what's good about a monolith. The monorepo, the shared database, the common library versions. But it sidesteps the four classic monolith failure modes:

  • Workloads scale and deploy individually. ETL doesn't have to keep up with web traffic. Web traffic doesn't pay for ETL headroom. Each function gets the compute it needs and nothing more.
  • Local development experience stays good. Every stack runs on a laptop. Every function runs on a laptop. Engineers can hit staging dependencies from their dev environment without spinning up the entire backend.
  • Large code changes are straightforward. Cross-cutting refactors land in one PR, because everything is one repo in one language with shared libraries. No "we need to coordinate three service owners and four migrations" meetings.
  • Costs scale linearly with traffic. Idle stacks don't burn money. Spiky workloads don't force you to over-provision the baseline.

The trade-offs we accept in return: cold-start latency on infrequently-invoked functions, the operational quirks of whichever serverless runtime you pick, and a harder time profiling cross-function execution paths. Every architecture pays some tax. We think this one is the right tax for our shape of company.

Upcoming challenges

This setup is dependent on the organization's size, and we expect to hit fresh problems as we grow. Three we're already watching:

Service-to-service discipline

The serverless monolith trades infrastructural migrations for domain-driven design discipline. Without architectural guidelines, core services start depending on each other in ways that make the codebase brittle. Fixing it is now a refactoring exercise rather than a costly migration, which is cheaper but still requires ownership. We expect to spend more time on service boundary docs and code review heuristics as the team scales.

CI pipeline scaling

With a monorepo, every PR potentially touches every test suite. We'll need to invest in CI that runs only the tests relevant to the changed code, otherwise pipeline time becomes the new velocity ceiling.

Database scaling

The OLTP database is shared across stacks. Eventually we'll have too much customer-generated data for that to be free. The good news: that's a "successful company" problem, and a lot of it can be pushed back by using the database well in the first place. Not doing ETL on the OLTP database is the easy win.

As it stands, the gains are far outstripping the losses. The architecture lets us ship faster, run cheaper, and onboard new engineers without a multi-week setup ritual. That's the whole point.

More from engineering

Read more on the Metaview Engineering blog.

How we build the systems that power live interview capture, structured scorecards, and ATS sync at scale.

Frequently asked questions

What is a serverless monolith?

A serverless monolith is an architecture that keeps the structural simplicity of a monolith (one repo, one language, one shared database, common library versions) while deploying and scaling each workload as an independent group of serverless functions. The deployment unit is the stack, not the domain, so workloads scale on their own load profile without forcing you to split the domain into micro-services.

Why not just start with micro-services?

Because domain boundaries shift faster than infrastructural ones, especially in an early-stage company. Splitting a business that's still finding its shape into a fixed set of micro-services tends to produce expensive migrations every time the domain re-aggregates. A serverless monolith lets you reshuffle deployment boundaries cheaply as the business changes.

Does the shared database become a bottleneck?

Eventually it will, in any architecture with a shared OLTP database. The mitigations are well-known: route ETL and analytical workloads to a dedicated analytical store, lean on read replicas for read-heavy paths, and shard or partition the OLTP store when scale demands it. Most of the runway comes from using the database well in the first place.

What does this trade off compared to a classic monolith?

You take on the operational quirks of a serverless runtime: cold-start latency on infrequently-invoked functions, runtime-specific limits on execution time and payload size, and harder cross-function profiling. In exchange you get independent scaling and deployment per workload, linear cost-to-traffic, and a much shorter local development setup.

When does the serverless monolith stop working?

It stops working when domain boundaries inside the monorepo become unmanageable through code review and architectural guidelines alone, or when independent teams need full ownership of their deployment cadence, on-call rotation, and language choice. At that point the shared monorepo and shared database become coordination ceilings, and a real micro-services split becomes the cheaper option.