Our services

If you can think it, we can make it brainsoft.

When a monolith is still the right call

Written By: BrainSoft In Engineering

Every few months someone on a team I work with proposes splitting the product into services. The reasoning is usually the same: the codebase is getting messy, deploys feel risky, and microservices are what serious companies do. Sometimes that is correct. Often it is a way to trade one set of problems for a harder, more expensive set.

A monolith is still the right call when you have one team, an unclear domain, and no scaling bottleneck that a bigger box or a read replica cannot fix. Start with a single deployable, keep module boundaries clean inside it, and split only when a specific part of the system has a reason to live on its own.

What a monolith actually costs you

The complaints about monoliths are real, they just get blamed on the wrong thing. A tangled codebase is a design problem, not a deployment-topology problem. Moving that tangle into twelve services gives you twelve tangles plus a network between them.

What you do pay for with a single deployable:

  • One build and one deploy pipeline. That is a cost, but it is a cost you pay once, not once per service.
  • Any change ships the whole app. With a small team this is usually fine and occasionally annoying.
  • Scaling is all-or-nothing. You scale the whole process even if one endpoint is hot.
  • One language and one runtime. That constrains you, and it also means any developer can read any part of the code.

Against that, you get transactions that actually work, no distributed tracing to set up before you can debug a request, and a local dev environment that is a single command. When we onboard someone onto a well-structured monolith, they are productive in days.

The signals that say keep it together

We look for concrete signals rather than a feeling. If most of these hold, splitting now is premature:

  • One team owns the whole product, or two teams that talk daily.
  • The domain model is still moving. You are renaming core entities every few weeks.
  • Traffic fits comfortably on a couple of application servers, and the database is the first thing to strain.
  • Deploys take minutes, not hours, and rollbacks are a single command.
  • Nobody has asked for a separate release cadence for one part of the system.

That last point matters more than people expect. The strongest argument for extracting a service is that a piece of the system genuinely needs to ship on its own schedule, or needs a different runtime, or has a failure mode you must contain. Everything else is usually solvable inside the app.

If the database is the bottleneck, that is a database problem. Add an index, add a read replica, cache the expensive query. None of that requires a service boundary.

Keeping the monolith from rotting

The reason monoliths get a bad reputation is that people let them rot. The fix is boring and it works: enforce module boundaries in code, not in prose. In a Python codebase we might use import linter rules; in TypeScript, an ESLint boundary plugin. The point is that a module can only reach into another module through its public entry point.

# .importlinter
[importlinter]
root_package = app

[importlinter:contract:layers]
name = Layered architecture
type = layers
layers =
    app.api
    app.services
    app.repositories
    app.models

Now the boundary is checkable in CI. When someone tries to import a repository directly from an API handler, the build fails and they get a clear message. That is the same discipline you would need with services, minus the network.

Two more habits that keep a single deployable healthy. First, keep the app stateless so you can run several copies behind a load balancer; sessions go in Redis or a signed cookie, uploads go to object storage. Second, make the background work a separate process reading from the same codebase and the same database. That gives you most of the operational benefit of a worker service without a second repo.

# same image, different command
docker run app:latest ./bin/web
docker run app:latest ./bin/worker

When we do split, and how

We split when there is a reason we can name in one sentence. Examples from real projects: a video transcoding pipeline that needs GPUs and long-running jobs; a billing integration that has to be audited separately; a search index that needs its own scaling curve. In each case the new service had a distinct resource profile or a distinct reason to fail independently.

The extraction itself is unglamorous. You put a seam in the monolith first, usually an interface with one implementation, and route all calls through it. Then you move that implementation behind an HTTP or queue boundary and keep the interface. If the seam was clean, the callers do not change. If it was not clean, you just learned something valuable before paying for a network hop.

This is also where a lot of teams ask for help, because doing it well means CI changes, observability, and deployment work at the same time. If that is where you are, our services cover that ground, but you can also do the first extraction yourself and see how it feels.

The honest summary: a monolith is a default, not a compromise. It stops being the right default when a specific, nameable part of your system needs to be independent. Until then, keep one repo, one pipeline, and hard module boundaries. You will ship faster, and you will still be able to split later because the seams are already there.

Frequently asked questions

Doesn't a monolith make it impossible to scale?

No. A stateless app behind a load balancer scales horizontally by running more copies, and most systems hit a database limit long before an application limit. Add indexes, caching, and read replicas first. If one specific workload needs a different scaling curve, extract that workload rather than the whole app.

How do we know when it is time to split something out?

When you can state the reason in one sentence. Separate release cadence, a different runtime or hardware profile, or a failure mode that must be contained. If the reason is that the code feels messy, that is a design problem you can fix inside the monolith, and it is cheaper to fix there.

Can a monolith be deployed safely with a small team?

Yes, and it is usually easier than the alternative. One pipeline, one artifact, one rollback command. Add automated tests, run migrations separately from the deploy, and ship behind a feature flag so a bad change can be turned off without a rollback. That covers most of the risk people associate with monolith deploys.


#Engineering