Uncategorized

How to Split a Monolith Into Microservices Without Killing Your Product in the Process

Splitting a monolith isn't a technical win — it's a business risk. Here's how Australian founders can sequence the decomposition without stalling their product.

Most teams get this backwards. They decide to break up the monolith because the architecture feels messy, not because a specific operational problem demands it. Then they spend six months rebuilding the same product they already had, shipping nothing new, and wondering why the business hasn’t moved. The monolith wasn’t the problem. The sequencing was.

We’ve worked through this with enough Australian startups – fintech, healthtech, B2B SaaS – to have a strong opinion: a premature microservices migration is one of the most expensive architectural mistakes a founder can make. Not because microservices are wrong, but because most teams attempt the split before the signals are loud enough to justify it, and without a clear strategy for what to extract first and why.

This is the honest guide to doing it in a way that doesn’t kill your product.

The Signal Problem: How You Know It’s Actually Time

Pain is not a signal. “The code is hard to work in” is not a signal. Every codebase older than eighteen months is hard to work in – that’s not architecture, that’s entropy.

The real signals that decomposition is worth the pain:

  • Independent scaling needs. One part of your system is under disproportionate load and you can’t scale it without scaling everything. A notifications service that fires millions of jobs shouldn’t share compute with your billing engine.
  • Deployment bottlenecks costing you real release time. If two teams are stepping on each other’s deployments and your release cadence has dropped to once a fortnight because of merge conflicts and regression risk, that’s a structural problem.
  • Compliance or data isolation requirements. If you’ve landed an enterprise customer that needs their data physically isolated, or you’re approaching a compliance boundary (GDPR, APRA, healthcare), you may need service separation you can actually point to in an audit.
  • Domain logic that belongs to genuinely separate business functions. When two different product teams are fighting over the same tables and the schema has become a negotiation, it’s time.

If you’re not hitting at least two of these, hold. Refactoring internal module boundaries within the monolith will give you 80% of the benefit at 20% of the cost and risk.

The Strangler Fig Is the Right Mental Model – With One Caveat

The strangler fig pattern is well-known: wrap the monolith, route traffic incrementally to new services, strangle the old code out over time. It works. But the caveat nobody mentions is that it only works if you’re strict about not letting new features bleed into the monolith while you’re mid-migration.

We’ve seen this unravel a fintech build. They started extracting their payments domain into a standalone service, which was exactly the right call. But while that work was underway, the product team kept shipping into the monolith – new invoice logic, new reconciliation rules – all of which touched the payments domain. By the time the extraction was done, the monolith had grown a new tendril into the service they’d just separated. Two months of work, mostly undone.

The rule we enforce: when you draw the extraction boundary, you freeze feature development in that domain until the migration is complete. Yes, that’s a business conversation. Have it explicitly. The alternative is architectural debt on top of architectural debt.

What to Extract First – and What to Leave Alone

The temptation is to extract the biggest, most complex domain first because it causes the most pain. This is almost always the wrong move. Complex domains have the most coupling, the most shared state, and the highest risk of a botched cut. You’ll spend months on it, and if it goes wrong, the blast radius is enormous.

Start with what we call a low-coupling, high-independence service. Something that:

  • Has a clear, bounded interface – it takes inputs and produces outputs without needing to read from shared tables
  • Has its own data that isn’t read or written by anything else in the monolith
  • Has a well-understood failure mode – if this service goes down, the rest of the app degrades gracefully rather than falling over

In practice, this tends to be things like: email/notification dispatch, document generation (PDFs, reports), file processing pipelines, third-party sync jobs (CRM sync, Xero integration). These are real services, they carry real load, and extracting them teaches your team the operational patterns – service-to-service auth, async messaging, distributed tracing – without betting the product on the exercise.

Save the core domain – the thing your app is actually about – for last, when your team knows how to do this properly.

The Operational Overhead Nobody Prices In

A monolith running on a single EC2 instance or a Railway deployment is one thing to monitor, deploy, and debug. Fifteen microservices is a fundamentally different operational surface.

Before you commit to the migration, you need to have answered – and budgeted for – all of this:

  • Service discovery and routing. How do your services find each other? AWS App Mesh, a load balancer per service, Kubernetes ingress? Each has cost and complexity implications.
  • Distributed tracing. When a request fails across three services, you need to be able to reconstruct what happened. Datadog, Honeycomb, or AWS X-Ray all cost real money – budget roughly $500-$2,000/month AUD depending on trace volume once you’re at meaningful scale.
  • Per-service CI/CD pipelines. You can’t have one pipeline for fifteen services. Each service needs its own build, test, and deploy chain. This is manageable but it takes time to set up properly.
  • Service-to-service authentication. mTLS or JWTs between services? This is not optional if any of your services handle sensitive data, and getting it wrong is a security incident waiting to happen.
  • Increased infrastructure cost. Running ten services with their own containers, health checks, and redundancy is meaningfully more expensive than one app. Expect your monthly AWS or GCP bill to increase by anywhere from $800 to $3,000+ AUD per month depending on your architecture – before you’ve added traffic.

A founder we worked with last year was genuinely surprised when their monthly infrastructure cost doubled after a microservices migration that had been sold to them as “more efficient”. More efficient to scale, yes. More expensive to run at small-to-mid scale, almost certainly.

Sync vs Async: The Communication Decision That Shapes Everything

How your services talk to each other is the architectural decision that most teams underspecify. Get it wrong and you’ve traded monolith coupling for distributed coupling – which is strictly worse, because it’s harder to see and harder to fix.

The rough framework we use:

Use synchronous HTTP/REST (or gRPC) when: the calling service needs an immediate response to continue, the operation is short-lived, and you can tolerate the tight coupling.

Use async messaging (SQS, SNS, Kafka, or even a simple Postgres-backed queue) when: the downstream operation is slow, the caller doesn’t need to wait, or you need resilience to downstream failures. Order processing, notifications, data sync, file handling – async, always.

The mistake is defaulting to synchronous calls everywhere because they’re easier to reason about during development. You end up with a distributed monolith – every service blocking on every other service, with latency multiplying across the call chain. We’ve had to untangle this on projects we’ve inherited from other teams. It’s ugly work.

If in doubt, err async. You can always expose a synchronous facade over an async backend later. Going the other direction is much harder.

Sequencing the Migration Without Halting the Roadmap

The hardest part of this isn’t technical. It’s maintaining product momentum while the architecture changes underneath the product. Here’s how we sequence it in practice:

  1. Modularise internally first. Before extracting anything, draw hard module boundaries inside the monolith. Every domain gets its own folder, its own internal API, and a rule that nothing outside the module reads its database tables directly. This discipline alone eliminates most of the coupling pain and takes days, not months.
  2. Extract one low-risk service. Pick the candidate from the criteria above. Build the full operational pattern around it: CI/CD, monitoring, auth. Treat it as a reference implementation for everything that follows.
  3. Run the new service in parallel for a sprint. Route some traffic to it, keep the monolith code path alive as a fallback. Compare outputs. This is your insurance.
  4. Cut over, then delete the old code. Deletion is the win. If the old code stays, it rots back in.
  5. Repeat, in complexity order, not pain order.

Each extraction should take three to six weeks for a small team doing this properly. If a single service extraction is taking longer than eight weeks, something is wrong with the boundary you drew.

If you’re staring down this decision and not sure whether your product is ready for it – or whether it’s even the right call – talk to Amora about your build. Sometimes the best outcome from that conversation is a clear reason not to do it yet.

The goal is never microservices. The goal is a system that lets your team move fast without fear. Sometimes that’s a well-structured monolith. Know which one you actually need before you start cutting.

Got something you want built?

Amora Digital is an Australian software and AI agency. We scope it, build it, and ship it – live in 28 days. No offshore teams. No surprises.

Book a discovery call

Ready to stop guessing and start growing?

Book a 30-minute strategy call. No pitch, no pressure — just a clear read on what's working, what isn't, and where the lift is.

Book your strategy call