[ MODERNIZATION ] // MONOLITH → SERVICES

Decompose the monolith without breaking the business that funds it.

Strangler-fig migrations with explicit data ownership boundaries, OpenTelemetry from day one, and a realistic 18-36 month arc. We'll also tell you which modules to leave alone.

Veteran-Owned SDVOSB
[001 / 005] Field Conditions

Most microservices migrations fail in month 9, not month 1.

// SITUATION

The pattern is predictable. A team announces a microservices migration. The first two services ship in four months and morale is high. Then reality lands: the shared Postgres database still couples everything, there's no distributed tracing, deploys still require coordination across teams, and the monolith is now 95% of its original size plus two services that depend on it. Eighteen months in, leadership asks why the platform is slower and more expensive than before. The honest answer is that nobody addressed the prerequisites — observability, data ownership, deploy independence — before extracting code.

  • Services share a single Postgres instance with foreign keys across boundaries, so no service can deploy or scale independently.
  • No distributed tracing means a single failed checkout takes four engineers and three hours to diagnose across service logs.
  • Teams extracted services by code shape (controllers, models) instead of data ownership, recreating the monolith over HTTP.
  • Leadership was sold a 6-month timeline; at month 14 the monolith is intact and the new services are an unfunded liability.
18-36 mo
Realistic arc to stable target state
30%
Of modules typically stay in the monolith
< 4 wks
To deliver a decomposition readiness audit
[002 / 005] Operational Approach

Strangle the monolith. Don't rewrite it.

  1. STEP-01

    Map the seams before cutting

    We instrument the monolith with OpenTelemetry and run traffic analysis for 2-4 weeks. Output is a service candidate map ranked by call volume, transactional coupling, and team ownership. Modules with shared write paths to the same tables stay together until the data is split.

  2. STEP-02

    Build the platform first

    Before extracting service one, we stand up the boring prerequisites: CI/CD per service, a service mesh or API gateway, centralized logs and traces, a secrets store, and on-call rotations. Skip this and you ship 12 microservices into a single shared logging void.

  3. STEP-03

    Extract by data ownership, not code shape

    Each service owns its tables. Period. We use the strangler-fig pattern: route reads through a facade, dual-write during cutover, then flip writes. Foreign keys across service boundaries get replaced with event streams (Kafka, SNS/SQS) or async reconciliation jobs.

  4. STEP-04

    Cut the first service that hurts

    We pick a domain with painful deploy contention or a clear scaling asymmetry — usually billing, notifications, or auth. First extraction takes 3-5 months including platform work. Subsequent services compress to 6-10 weeks each as the paved road forms.

  5. STEP-05

    Stop when the math stops working

    Not every module should be a service. We explicitly mark modules that stay in the monolith — low change frequency, tight transactional coupling, single-team ownership. A modular monolith with 6 services beats 40 microservices nobody can debug at 2am.

// TYPESCRIPT PATTERN
// Strangler-fig facade: route traffic to old monolith or new service
// based on a feature flag, with dual-read verification during cutover.

import { LaunchDarkly } from './flags';
import { logger, metrics } from './observability';

export async function getCustomer(id: string): Promise<Customer> {
  const mode = await LaunchDarkly.variation('customer-svc-cutover', id, 'monolith');

  if (mode === 'monolith') {
    return monolithDb.customers.findById(id);
  }

  if (mode === 'shadow') {
    // Read both, return monolith, alert on drift. Run for 2+ weeks.
    const [legacy, next] = await Promise.all([
      monolithDb.customers.findById(id),
      customerService.get(id).catch((e) => { logger.warn({ e }, 'svc_err'); return null; }),
    ]);
    if (next && !deepEqual(legacy, next)) {
      metrics.increment('cutover.drift', { entity: 'customer' });
    }
    return legacy;
  }

  // mode === 'service' — monolith is now read-only fallback
  try {
    return await customerService.get(id);
  } catch (err) {
    metrics.increment('cutover.fallback');
    return monolithDb.customers.findById(id);
  }
}

Shadow reads with drift metrics are how you actually de-risk a cutover — not a Friday-night flip and a prayer.

[003 / 005] Common Questions

Field FAQ.

How long does a real monolith-to-microservices migration take?

For a non-trivial system — say 500K+ lines, 5+ years of accumulated logic — plan for 18 to 36 months to reach a stable target state. The 6-month timelines you see in conference talks are either greenfield rewrites mislabeled as migrations, or they ignored the long tail of edge cases. We size engagements in 90-day increments with explicit go/no-go gates so you can stop when the ROI flattens.

When should we NOT extract a service?

Don't extract modules that share transactional writes with the rest of the system unless you're prepared to redesign the consistency model. Don't extract modules owned by a single team that deploy infrequently — you'll add network failure modes for zero organizational benefit. Don't extract just because a module is 'big.' Size alone isn't a reason. Coupling, change frequency, and scaling asymmetry are the real signals.

What's the strangler-fig pattern in practice?

You put a facade (API gateway, reverse proxy, or in-process router) in front of the monolith. New functionality goes to new services behind that facade. Existing functionality gets carved out one route at a time: shadow read, dual write, flip write, decommission old code. The monolith shrinks gradually instead of getting rewritten. It's slower than a big-bang rewrite on paper but ships value continuously and is reversible at every step.

Do we need Kubernetes to do this?

No. Kubernetes is one option. ECS, Nomad, or even systemd on EC2 with a decent deploy pipeline all work for the first 5-15 services. We've seen teams burn 6 months learning Kubernetes when they had 3 services to run. Pick orchestration based on team operational maturity and the number of services you'll actually run within 18 months — not on resume-driven design.

How do you handle data ownership when splitting a service?

Each service owns its tables and is the only writer. Other services read via API or via events on a stream (Kafka, Kinesis, SNS/SQS). Cross-service joins move to either API composition at the edge, materialized read models, or CQRS projections. We typically split the database last — code and deploy boundaries first, then schema separation, then physical database separation. Splitting data first without a service contract creates the worst of both worlds.

What observability do we need before extracting service one?

Distributed tracing (OpenTelemetry to Honeycomb, Datadog, or Tempo), structured JSON logs in a single searchable store, RED metrics per service (rate, errors, duration), and correlation IDs propagated through every hop including the monolith. Without this, your first production incident across two services will take 4 hours to diagnose instead of 20 minutes. We refuse to extract a service into an environment that can't trace a request end-to-end.

Should we use AI tools to help with the decomposition?

Selectively. Claude and GPT are useful for summarizing legacy modules, generating first-pass dependency maps from code, and drafting service contracts from existing controllers. They're not useful for deciding service boundaries — that's a judgment call based on team structure, data flows, and business volatility. We use AI to accelerate the analysis phase by 30-50% but keep architects in the decision loop.

Is this work eligible for federal SDVOSB set-asides?

Yes. VooStack is SDVOSB-certified and routinely performs application modernization under set-aside vehicles for civilian and DoD agencies. Monolith decomposition fits naturally under modernization line items in IDIQ contracts and BPAs. We can engage as prime or as a subcontractor on a teaming agreement, and we hold the security posture (FedRAMP-aligned practices, US-based cleared engineers available) needed for sensitive workloads.

What does a realistic team structure look like during migration?

One platform team (3-5 engineers) owns the paved road: CI/CD, observability, service templates, deploy tooling. Two to four product teams (4-6 engineers each) own services aligned to business domains. A small architecture council (2-3 senior engineers) reviews service boundaries weekly. Smaller orgs can compress this — we've run successful migrations with 8 total engineers — but the platform/product split is non-negotiable. Everyone-does-everything kills migrations by month 9.

[ NEXT ACTION ]

Bring us your monolith. We'll tell you which 30% should never be extracted.

Talk to a VooStack operator. We respond within one business day.