Skip to main content

Hybrid and Evolving Architectures: Starting Modular, Extracting Services

What This Concept Is

Real systems rarely fit one style end-to-end. They are hybrids, and they evolve over time as requirements, teams, and scale change. This concept names the common evolution paths and the mechanics of moving between styles without breaking production.

Why hybrids are the norm

Different parts of a system have different characteristics:

  • The transactional core (checkout, accounts) wants consistency and testability -> layered or modular monolith.
  • The data platform (analytics, reporting, ETL) wants transformation and throughput -> pipeline.
  • The integration layer (notifications, webhooks, cross-system events) wants async fan-out -> event-driven.
  • The hot path (search, recommendations, bids) wants elasticity -> extracted microservice or space-based.
  • Internal admin and legacy glue -> modular monolith, sometimes layered.

Forcing a single style across all of these is worse than admitting they differ.

Common evolution paths (all documented in the industry)

  1. Monolith -> Modular monolith. The least-regret first move. Reorganize code into modules; introduce boundary enforcement (Concept 6). Zero new infrastructure required. Catches most "our monolith is painful" cases.
  2. Modular monolith -> Service-based. Extract 3-6 coarse services. Keep a shared DB. Each service deploys independently. This is where most teams should stop.
  3. Modular monolith -> Selective microservices. Extract only the modules that genuinely need independent deploy, extreme scale, or data isolation. Leave the rest as modular monolith. This is the strangler fig pattern, named by Martin Fowler after a tree that grows around a host until the host is gone.
  4. Layered -> Event-driven (partial). Keep the core layered; introduce events for async side-effects (emails, analytics, audit). A common first step.
  5. Layered -> Hybrid (layered + pipeline). Carve out the ETL part into a real pipeline. Rest stays layered.

The strangler fig pattern

Year 0:  [ modular monolith ]

Year 1: [ modular monolith ] <----proxy/facade---- [search service]
(all traffic except search goes to monolith)

Year 2: [ modular monolith ] <----proxy/facade---- [search service]
[recommendations service]
[pricing service]

Year 3: Smaller monolith + several independent services
(monolith still exists, much smaller)

The facade/proxy redirects traffic for extracted capabilities to new services. The monolith shrinks in place. No big-bang rewrite.

Why It Matters Here

Most of the worst architecture mistakes in industry are big-bang rewrites driven by "the current system is bad, let's replace it with the modern one." They almost never finish on time; they often fail outright. Understanding hybrids and evolutionary paths is what lets you say "we can improve this without betting the company."

This concept is also the bridge to M05 (ADRs and reviews): every evolution step is an ADR that should be written down and reviewed, not a slow drift.

Concrete Example

OrderFlow evolution over 3 years:

Year 0: Monolith. Ruby on Rails, 80k LOC, 20 engineers, weekly deploys, moderate pain.

Year 1, Q2: Modular Monolith. Introduce 6 modules (catalog, checkout, fulfillment, billing, notifications, admin). Add Packwerk. Enforce "no cross-module imports of internals." Result: deploy pain unchanged; onboarding faster; a year later, 400 boundary violations blocked in CI.

Year 2, Q1: First service extraction. Search. Extracted because (a) it runs differently (it is read-heavy, Elasticsearch-backed), (b) it is touched by a different team, (c) its deploy was blocking the main pipeline. Strangler fig: search queries start going to the new service; writes still happen in the monolith, mirrored to ES. After 2 quarters, search module is removed from the monolith.

Year 2, Q4: Event-driven notifications. Notifications split off from the monolith as an async subscriber to domain events. Kafka introduced. Monolith emits OrderConfirmed; notifications service consumes and sends. Buys fault isolation (notification outage does not block checkout) and flexibility (adding a Slack notifier does not require a monolith change).

Year 3: Hybrid. The system now has:

  • Modular monolith for checkout, catalog, billing, fulfillment, admin.
  • Search service (microservice, Elasticsearch).
  • Notifications service (event-driven, Kafka).
  • Reporting pipeline (pipe-and-filter, nightly).

The team did not "become microservices." They extracted specific capabilities where the tax was worth paying. The monolith is smaller, healthier, and still the center of the system.

Common Confusion / Misconception

"A hybrid is a sign of indecision." A hybrid is a sign of pragmatism. Different parts of the system genuinely have different needs. A consistent single style for everything is usually over-committing.

"Strangler fig is slow." It is slower than a fantasy big-bang rewrite that never ships. It is much faster than a big-bang rewrite that ships, because it actually delivers incremental value and keeps the product alive.

"If we are going to end at microservices, start with microservices." No. The first cut of service boundaries is almost always wrong because you do not yet understand the domain. Modular monolith lets you discover boundaries with the lowest cost of being wrong. Then extract the ones you are confident about.

"A rewrite is cleaner." Rewrites fail at a famously high rate. The original system captures years of edge cases that are not documented anywhere except in the code. A strangler migration preserves those edge cases automatically; a rewrite rediscovers them in production.

"Evolution has to happen in one direction." Styles can de-evolve too. Netflix famously talks about re-merging over-split microservices back into "macroservices." If your extraction was wrong, merging back is a valid move.

How To Use It

A playbook for any "our system is hurting" conversation:

Concrete rules:

  1. Never big-bang rewrite if a strangler is possible. A strangler is almost always possible.
  2. Every extraction is an ADR. Name the trigger, the expected benefit, the tax, the rollback plan.
  3. Measure after. If extracted services do not deploy independently in practice within one quarter, you did not have microservices criteria met (Concept 12). Merge back or pause.
  4. Accept asymmetry. It is fine for one module to be extracted and others to remain. "We are not consistent" is not the right value to optimize for.

Check Yourself

  1. What is the strangler fig pattern? Describe it in three sentences.
  2. Why does starting with microservices usually produce wrong boundaries?
  3. A team proposes extracting 4 services in one quarter. What would you ask them, and what order would you recommend?

Mini Drill or Application

Take one real system. In 30 minutes:

  1. Identify the current style (layered, modular monolith, service-based, etc.).
  2. Identify the top two pain points and name the characteristic each represents.
  3. Propose one evolutionary step (not a full rewrite) that addresses one of them.
  4. Write a short ADR: decision, trigger, expected benefit, cost/tax, rollback plan.

Read This Only If Stuck