Monolith to Microservices Decomposition

Senior Java Developer · 2023 · 8 months · 4 people · 5 min read

Decomposed a monolithic god-service into 5 focused microservices, reducing deployment risk and enabling independent team scaling

Overview

A strategic initiative to break apart a large monolithic service that had grown into an unmaintainable 'god-service' handling authentication, business logic, reporting, notifications, and third-party integrations in a single deployable unit. The decomposition aimed to enable independent deployments, improve fault isolation, and allow different parts of the system to scale independently.

Problem

The monolithic service had grown organically over several years, accumulating tightly coupled modules that made every deployment a high-risk event. A bug in the reporting module could bring down the authentication system. Scaling required scaling everything, even if only one module was under load. Developer productivity suffered because merge conflicts were constant, test suites took over 40 minutes, and reasoning about the codebase required understanding the entire system.

Constraints

  • The decomposition had to happen incrementally while the monolith continued serving production traffic
  • No downtime budget; the service was business-critical with 24/7 availability requirements
  • Small team of 4 developers meant we could not parallelize the work across all modules simultaneously
  • Existing clients consumed the monolith's API, so external contracts had to be preserved

Approach

We followed the Strangler Fig pattern, progressively extracting bounded contexts from the monolith into standalone microservices. Each extraction followed a consistent playbook: identify the bounded context boundaries, introduce an anti-corruption layer, extract the data model, migrate traffic gradually using feature flags, and finally remove the dead code from the monolith. Inter-service communication used a combination of synchronous REST for queries and Apache Kafka for event-driven workflows.

Key Decisions

Used the Strangler Fig pattern for incremental migration rather than a rewrite

Reasoning:

A full rewrite would have required feature-freezing the monolith for months, which was unacceptable to the business. The Strangler Fig approach let us extract services one at a time while continuing to deliver features on the monolith.

Alternatives considered:
  • Big-bang rewrite into microservices during a feature freeze
  • Branch-by-abstraction within the monolith without extracting services

Introduced an API gateway to preserve external client contracts

Reasoning:

External clients consumed the monolith's API endpoints directly. An API gateway allowed us to route requests to the appropriate microservice transparently, so clients experienced no breaking changes during the migration.

Alternatives considered:
  • Versioned API with client migration plan
  • Client-side service discovery

Extracted the notification service first as the lowest-risk bounded context

Reasoning:

Notifications had the clearest boundaries, fewest cross-module dependencies, and a natural asynchronous interface. Starting with the easiest extraction let us validate our playbook and build team confidence before tackling more tightly coupled modules.

Alternatives considered:
  • Start with authentication as the most critical service
  • Extract reporting first due to its resource-intensive nature

Tech Stack

  • Java 17
  • Spring Boot
  • Apache Kafka
  • REST APIs
  • Docker
  • PostgreSQL
  • Feature Flags
  • API Gateway

Result & Impact

  • Increased from bi-weekly to multiple times per day per service
    Deployment Frequency
  • Reduced from 40+ minutes (monolith) to under 5 minutes per service
    Test Suite Duration
  • Production incidents isolated to individual services, no more cascading failures
    Incident Blast Radius
  • 5 microservices from 1 monolith over 8 months
    Services Extracted

The decomposition fundamentally changed how the team worked. Independent deployments meant developers could ship features without coordinating with the entire team. Fault isolation eliminated the anxiety around deployments, and the focused codebases made onboarding new developers significantly faster. The architecture also enabled different services to scale independently based on actual load patterns.

Learnings

  • The Strangler Fig pattern is powerful but requires discipline. The temptation to extract too many things at once must be resisted; one bounded context at a time keeps risk manageable
  • Data decomposition is the hardest part of service extraction. Shared database tables create hidden coupling that is not visible in the application code
  • Feature flags are essential for safe traffic migration, but they accumulate as technical debt if not cleaned up promptly after migration completes
  • Safe integration with third-party APIs benefits enormously from the microservice boundary. Isolating external dependencies behind dedicated services prevents third-party outages from cascading through the system

Technical Deep Dive

The most challenging extraction was the business logic module, which had tendrils reaching into nearly every other part of the monolith. We spent two weeks mapping the dependency graph before writing a single line of migration code, using static analysis tools and runtime call tracing to identify every interaction point. The key insight was that what appeared to be a single “business logic” module was actually three distinct bounded contexts: order processing, pricing rules, and customer management. Recognizing these sub-boundaries allowed us to plan a phased extraction rather than attempting to pull out the entire module at once.

Data decomposition proved to be the most technically demanding aspect of the entire project. The monolith used a single PostgreSQL database with foreign key relationships spanning modules. We could not simply split the database along service boundaries without breaking referential integrity. Our approach was to first introduce logical separation through schema namespacing, then replace cross-module foreign keys with application-level references using eventual consistency via Kafka events. Each service maintained its own read model of the data it needed from other services, updated asynchronously through event consumers. This required careful handling of ordering guarantees and idempotency in our event processors.

The API gateway layer was critical for maintaining backward compatibility with existing clients. We used a routing configuration that started by directing all traffic to the monolith, then gradually shifted specific endpoint groups to the extracted microservices using weighted routing. Feature flags controlled the routing weights, allowing us to start at 1% traffic to a new service, monitor error rates and latency, and progressively increase to 100%. This canary-style migration caught two subtle behavioral differences between the monolith and extracted services that would have caused client-visible issues if we had cut over all traffic at once.