Mar 24, 2025·8 min read

Monolith ownership problems to fix before microservices

Monolith ownership problems often show up as messy deploy rights, wide test scope, and blurry module lines. Fix them before splitting services.

Monolith ownership problems to fix before microservices

Why the monolith feels slow and risky

A monolith does not become slow just because it is one codebase. Plenty of teams ship from a single application for years and do fine. They move quickly because each part of the product has a clear owner. One team handles billing, another owns auth, another looks after the admin area. People know who can change what, who reviews it, and who answers when something breaks.

Problems start when that ownership gets blurry. A small change in one screen touches shared code, then three teams review it, then nobody feels safe approving the deploy. The code itself might be easy to edit. The path to production is what feels crowded.

Teams often blame architecture first. They say the monolith is too big, too old, or too tangled. Sometimes that is true. Just as often, the bigger problem is that nobody has clear control over a module. When ownership is fuzzy, every change turns into a group decision, and group decisions are slow.

Code size matters less than ownership clarity. A large monolith with clean module boundaries can move faster than a small system split into services that several people half-own. When one team can change, test, and ship its area without asking five others, the monolith stops feeling like a trap.

Take a checkout bug that needs a one-line fix. An engineer makes the change in ten minutes. Then the pull request waits for reviews from payments, platform, and QA because the code lives in a shared folder that everybody touches and nobody owns. By the time it ships, the team says, "We need microservices." The repo is not the first problem. The team never settled who controls checkout.

That is why monolith pain often feels like technical debt even when it mostly comes from team design. If deploy rights are unclear, test scope spills across the whole app, and modules cut across several teams, the monolith will feel risky no matter how neat the code looks.

Signs you have an ownership problem

Ownership problems rarely look dramatic at first. The product still works. Releases still happen. But normal changes start to feel slow, risky, and strangely political.

One clue is overlap. Two teams change the same module in the same week, not because they planned the work together, but because nobody sees a clean border around that code. You get merge conflicts, regressions, and tense handoffs. The monolith is part of the story, but the deeper issue is that no single team treats that area as theirs.

Deploys expose the same problem fast. Ask a simple question: who approves a production deploy for this part of the system? If the answer turns into a long thread, ownership is blurry. Teams wait for the one person who "usually knows this part," or they push the call to a manager who should not have to guess.

Tests tell the story too. A small edit in one module should stay small. If changing a form, a query, or one business rule wakes up most of the test suite, your code is too tangled for clear ownership. People stop making narrow fixes because nothing feels narrow anymore.

Bug reports bounce around in teams like this. Support sends an issue to team A. Team A says the bug lives in shared logic, so they pass it to team B. Team B finds a side effect in another module and sends it back. Two days later the customer is still waiting, and nobody owns the full fix.

Even names can give the problem away. A module called "billing" now contains permissions, emails, admin settings, and random subscription code. A folder named "core" usually means the team stopped agreeing on what belongs where. When names no longer match the work inside, ownership already slipped.

A quick test helps. Pick one module and ask three people what it owns, who reviews changes, who can deploy it, and which tests must pass. If their answers do not match, splitting that code into a service will not solve much. It will spread the same confusion across more moving parts.

Fix deploy rights first

When releases feel slow, teams often blame the monolith. A lot of the time, the first blocker is simpler: nobody knows who can ship, who can undo a bad release, and who can change production settings without asking around.

Start with a plain ownership map for each product area. For every module, name one team that makes the release call. Then write down who can merge code, deploy it, roll it back, and change config. If those answers live in chat history or in one senior engineer's head, you will keep hitting the same wall.

One team per area matters more than most teams think. If checkout belongs to two teams, it usually belongs to neither. Shared responsibility sounds fair, but it slows decisions and spreads blame when something breaks. Give one team the final say for each part of the monolith, even if other teams still contribute code.

Shared libraries and database changes need simple rules. Keep them boring. A shared library change can require approval from the module owner it affects. A database change that touches more than one module can require a written rollback plan. That is enough for many teams.

Keep emergency access narrow and visible. Do not hand broad deploy or config rights to half the company "just in case." A short list works better. Make every emergency action easy to trace, then review it the next day while the details are still fresh.

After each incident, check whether the owner list still matches reality. If the wrong people got paged, if nobody felt safe rolling back, or if three teams argued over a config switch, fix that before you talk about splitting services.

Many monolith problems look technical from the outside. Often they are permission problems with code wrapped around them.

Cut test scope to module size

A monolith gets scary when every change wakes up the whole test stack. If a small edit in billing triggers thousands of tests across login, search, admin, and reporting, the problem is not only speed. It is ownership. Nobody can tell which team broke what, so every failure feels like shared weather.

Split tests by module before you worry about frameworks. Each module should have its own fast suite that checks its rules, data access, and public behavior. The whole app still needs end-to-end coverage, but that suite should stay small and focus on a few important flows.

A simple setup works well:

  • Module tests run on every commit.
  • Contract or API tests check how modules talk to each other.
  • Whole-app smoke tests run less often, such as before release or on a schedule.

This saves time, but the bigger gain is clarity. When the orders module fails its own suite, the orders team knows it owns the fix. When one giant red build fails, everyone stares at it and waits.

Cross-module tests create a lot of noise. Teams add them with good intentions, then forget them. Months later, a change in user profiles breaks a reporting test that also touches permissions, email, and exports. That test no longer tells you one useful thing. It tells you five vague things badly. Delete it, or split it into module tests plus one narrow integration check.

Test names matter too. If a failing test does not tell you which module owns the problem, it is harder to act on. Clear test scope gives teams faster feedback and clearer responsibility.

Draw module boundaries people can keep

Audit One Painful Module
Start with checkout, billing, or reporting and make ownership real.

Most ownership trouble starts long before any service split. The code grows around frameworks, so teams end up with folders like controllers, services, utils, and models. That looks tidy at first, but it hides the actual business job.

Group code by what the product does, not by the layer it sits in. A module should map to something a non-engineer can name: billing, onboarding, pricing, reporting, inventory. If a team can describe a module without mentioning the tech stack, the boundary is usually easier to keep.

Names matter more than teams admit. "Backend," "core," "shared," or "misc" do not tell anyone where a change belongs. "Subscription billing" or "customer import" does. Renaming vague folders and packages feels small, but it removes a lot of daily confusion.

Each module also needs a clear way in and a clear way out. Other parts of the app should know how to call it, what data they can send, and what comes back. If developers have to reach into private files or copy internal logic, the boundary is not real.

Database access is where many monoliths fall apart. One module reads another module's tables, writes a few columns, then adds a shortcut because it is quicker. A month later, nobody knows who owns the rule. Keep direct database access inside the module that owns that data whenever you can. Let other modules ask through code instead of reaching across the line with queries.

A cleanup pass does not have to be dramatic. Rename folders nobody can explain. Move code next to the business job it supports. Cut down cross-module database writes. Keep a small public API for each module. Then write one sentence that starts with "This module owns..."

That sentence does more work than it seems. "This module owns plan changes and billing state for active subscriptions" is clear. If a team cannot write a sentence like that, the boundary is still fuzzy, and splitting it into a service will only spread the mess around.

A simple example from a growing product

Checkout is where many growing SaaS products hit this first. A customer pays with a card, gets the wrong price, misses the receipt email, and triggers a fraud review by mistake. The code for that flow lives in one monolith, but four teams touch it. Pricing changes one rule, billing changes another, marketing edits email timing, and a platform team approves the deploy.

That setup feels like a technical issue, but it is mostly an ownership issue. Nobody owns the whole outcome, so every change turns into handoffs, waiting, and blame.

A better move is smaller than a service split. Give one team full responsibility for pricing and billing changes from start to finish. That team does not need to own every part of checkout forever. It needs a clear area where it can make changes, run tests, and ship without asking three other groups for permission.

Then move the test scope closer to that area. Instead of running a huge suite that touches the whole product for every pricing change, keep a focused set of tests around checkout totals, payment success and failure, receipt emails, and fraud flags. Release checks should answer one question: did this change break the pricing and billing path?

The rest of the monolith can stay where it is. You do not need new network calls, new deployment pipelines, or a new on-call rotation just to fix unclear ownership. You need clearer module boundaries, faster deploy rights, and tests that match the area a team actually controls.

If coordination still hurts after that, a service split may make sense later. By then, the split is easier because the ownership line already exists in the code and in the org chart. That is a much safer point to cut.

A practical plan for the next month

Review Your Monolith Risks
Find where team structure slows changes before you add more services.

A month is enough to find out whether you need a new service or better ownership inside the monolith. Keep the scope tight. Pick one painful area, such as checkout, user accounts, or reporting, and use it as the test case.

In week one, make a map. Write down the modules involved, who changes them, who approves deploys, and who can roll back a bad release. Keep it to one page. If three people can ship changes but nobody feels responsible when something breaks, you found a real ownership gap.

In week two, fix test scope. Separate fast module tests from slow whole-app tests. A developer should be able to change that area and get useful feedback in minutes, not wait for unrelated checks across the codebase. If one small change still needs the full test suite, the problem is not a lack of microservices.

In week three, make the boundary real. Look for calls across modules that exist only because the code grew that way over time. Move shared logic behind clear entry points. Remove shortcuts into other modules. Stop reaching into internal data structures from the outside. One team should be able to change this area without touching three neighboring parts of the app.

In week four, run a deploy drill. The owner for that area should rehearse a normal deploy and a rollback during working hours. Time it. See who has to join, which checks still depend on tribal knowledge, and where the process gets stuck. A service split will not fix messy deploy rights. It often makes them harder to see.

Then decide from evidence. If the area has one owner, fast tests, clear boundaries, and a calm deploy path, ask whether a separate service solves a real problem like different release timing or different scaling needs. If those basics still feel shaky, keep fixing the monolith first. Splitting early just moves the confusion onto the network.

Mistakes teams make when they split too soon

Teams often split a service to escape arguments inside a monolith. That rarely works. The same ownership trouble shows up after the split, except now it breaks over the network.

The first mistake is simple: nobody owns the new service. One team creates it, two other teams depend on it, and everyone assumes someone else will maintain it. Small bugs sit for days. Urgent changes turn into Slack debates. If one team cannot say "this is ours" and back it up with deploy access, backlog time, and on-call duty, the service is not ready to exist.

Another mistake is moving shared tables behind an API on day one. On paper, that looks cleaner. In practice, the same messy data model still drives everything, but now every read and write needs a remote call. A checkout module that once queried one table now waits on a new service, retries on timeout, and fails in places that used to be boring and fast.

Teams also copy their giant test suite into every new service. That feels safe for a week, then every small change triggers long pipelines across multiple repos. People stop trusting the tests because the noise gets too high. A new service needs a smaller test scope, not all the monolith's baggage.

API calls do not fix team conflict either. If two teams cannot agree on data rules, release timing, or bug priority inside one codebase, they will not suddenly agree because the boundary now uses JSON. The conflict just gets harder to trace.

The operational work gets ignored most often. A new service needs someone to carry the pager, define rollback steps, own incidents and postmortems, and keep alerts useful instead of noisy. If those jobs are still blurry, wait.

A service split should reduce confusion. If it spreads confusion across more repos, more deploys, and more people, the split came too early.

A quick check before you create a service

Draw Better Module Boundaries
Group code by business job so teams can change and ship with less waiting.

A new service helps only when one team can run it end to end. If the code leaves the monolith but ownership stays fuzzy, you just move the same delay into a new place.

Use a short check before you split anything:

  • One team can say what it owns in one clear sentence.
  • That team can deploy on its own without waiting for two other teams.
  • Tests for that area finish quickly, and failures point to one module instead of half the app.
  • The module exposes a small public surface and keeps most internals private.
  • The split removes handoffs and approval loops instead of just moving files around.

If even one of those points is shaky, stop there. Fix the team boundary first. Fix deploy rights next. Then shrink test scope until the team gets fast feedback from its own code.

A common bad sign looks like this: a team says it owns billing, but every release still needs help from platform for config changes, help from auth for shared code, and a full regression run because tests touch unrelated areas. That team does not own billing yet. It owns a ticket queue.

Module boundaries matter just as much. When a module hides its internals, other parts of the app depend on a few clear entry points. That makes bugs easier to trace and changes safer to ship. If other teams reach into its tables, classes, or helpers, a service split will hurt more than it helps.

Code size alone is a weak reason to split. A large module with clear ownership is often easier to manage than a small service that needs three teams to change one field. Create a service when it cuts coordination. If it only changes where the code lives, keep it in the monolith and clean up the boundary first.

What to do next if the team stays stuck

Most monolith problems do not need a new service first. They need one module with a clear owner, a clear deploy path, and tests that stay inside that module.

Pick the part of the product that causes the most release friction. Make one person directly responsible for it for the next month. That does not mean they write every line of code. It means they approve changes, watch the tests, and make the final call on release readiness.

Keep the first pass small. Choose one module with frequent changes, give it one owner or one small pair, define one deploy path for that module, and write down which tests must pass before release.

Then use those rules in real releases. A rule that lives only in a doc is useless. If the owner still needs three approvals from unrelated teams, you did not fix deploy rights. If every test in the repo still runs for a tiny change, you did not fix test scope. If people still argue about where the module starts and ends, the boundary is still fuzzy.

Write down what happens after two or three releases. Note where the team waited, who blocked the change, and which tests slowed things down. Those notes matter more than another architecture debate.

If the same boundary questions keep coming back, an outside review can help. Oleg Sotnikov at oleg.is works as a Fractional CTO for startups and small businesses, helping teams sort out architecture, release flow, and ownership before they add more services. A short review is often cheaper than spending weeks moving code into a split that keeps the same human problems.

Frequently Asked Questions

Is the monolith really the problem?

Not always. Many monoliths work well for years when one team clearly owns each product area and can change, test, and ship it without a long approval chain.

If every small fix needs several teams, broad test runs, or unclear deploy approval, your structure around the code likely hurts more than the repo shape itself.

Why does a monolith start to feel slow and risky?

Most teams feel risk when ownership is fuzzy. A tiny change touches shared code, several people review it, and nobody feels safe pressing deploy.

That makes normal work slow and political. The code may look fine, but the path to production feels crowded.

How can I tell if we have an ownership problem?

Pick one module and ask three people four things: what it owns, who reviews changes, who can deploy it, and which tests must pass. If their answers differ, ownership is not clear.

You can also watch bug flow. When support tickets bounce between teams or deploy approval turns into a long chat thread, the problem usually sits in ownership, not repo size.

Can two teams own the same module?

No. One team needs the final say for each area. Other teams can still contribute, but one owner must decide on changes, releases, and rollback.

When two teams share final responsibility, decisions slow down and blame spreads fast when something breaks.

What should we fix before we split into microservices?

Start with deploy rights, test scope, and module boundaries. Write down who can merge, deploy, roll back, and change config for the area that hurts most.

Then make tests match that area and clean up the boundary so other parts of the app stop reaching into its internals. A service split works better after those basics are clear.

How should we change our tests in a monolith?

Keep most tests close to the module that changed. A small edit in billing should not wake up login, search, admin, and reporting unless the change truly touches them.

Run fast module tests on each commit, keep a few narrow checks between modules, and use a small whole app smoke suite for release confidence. That gives faster feedback and clearer responsibility.

What does a good module boundary look like?

A good boundary follows a business job, not a framework layer. Names like billing, onboarding, pricing, or reporting give teams a clearer target than folders like core or utils.

The module should expose a small public surface and keep its own rules and data access inside. If other teams need private files, helper functions, or direct table writes, the boundary is weak.

When does a service split actually make sense?

Split when one team already owns the area end to end and the split removes real coordination pain. Good reasons include different release timing, different scaling needs, or a boundary the team already keeps inside the monolith.

Do not split just because the code feels big. Large code with clear ownership is often easier to run than a small service that needs three teams for one change.

What goes wrong when teams split too early?

Teams often move shared data behind an API too early, copy huge test suites into new repos, and forget who will carry incidents and deploys. That trades one kind of confusion for another.

The same team conflict also survives the split. If people cannot agree on rules or release timing inside one codebase, JSON over the network will not fix that.

When should we get outside help?

Bring in outside help when the same release friction keeps coming back after a few real fixes. If deploy rights stay unclear, tests still spread across the whole app, or teams still argue about where a module starts and ends, a fresh review can save time.

A short architecture and ownership review usually costs less than moving code into new services that keep the same human problems.