DEV Community

Alex Aslam
Alex Aslam

Posted on

When to Split a Monolith: The Art of Surgical Extraction

Your monolith isn’t "bad"—it’s just outgrowing its skin.

One day, you wake up to:

  • 45-minute test suites because "everything depends on everything."
  • Deployments feel like Jenga—move one piece, and the whole tower wobbles.
  • Two teams waiting on the same migration, cursing each other in Slack.

But before you jump to microservices, let’s ask: Should you split? And if so, how?


When to Split: The 5 Catalysts

1. Bounded Contexts Are Choking

Sign: Your User model handles:

  • Authentication (Devise)
  • Profile management (Avatars, preferences)
  • Billing (Subscriptions, invoices)
  • Analytics (Track logins, activity)

Solution: Split into:

  • Identity Service (Auth, roles)
  • Billing Service (Subscriptions, payments)
  • User Profiles Service (Bio, avatars)

Pattern: Follow Domain-Driven Design (DDB) boundaries.

2. Scaling Constraints Hit

Sign:

  • One database table (e.g., events) eats 80% of your CPU.
  • Teams argue over schema changes.

Solution: Extract the hot table into a service with its own DB.

Example:

Before: Monolith → PostgreSQL `events` table
After:  Monolith → Events Service (with dedicated PostgreSQL)
Enter fullscreen mode Exit fullscreen mode

3. Team Silos Form

Sign:

  • Team A owns "Orders," but Team B keeps breaking them via "Inventory" changes.
  • Merge requests require 5+ reviewers from different teams.

Solution: Extract services along team boundaries (Conway’s Law).

4. Third-Party Dependencies Multiply

Sign:

  • Your monolith directly calls:
    • Stripe (payments)
    • Twilio (SMS)
    • SendGrid (email)
  • Every API change forces app-wide updates.

Solution: Extract a "Partner Gateway" service to consolidate external calls.

5. Performance Isolation Needed

Sign:

  • A background report job locks the DB, killing checkout performance.
  • You can’t scale the hot path independently.

Solution: Move batch jobs to a separate service.


How to Split: 3 Incremental Patterns

1. Strangler Fig Pattern

Idea: Gradually replace monolith pieces with services.

Steps:

  1. Proxy requests: Route /api/v2/orders to new Orders Service.
  2. Sync data: Use event streaming (Kafka) or DB triggers.
  3. Kill the old path: Sunset /api/v1/orders once v2 works.

Tools:

  • nginx for routing
  • delegate gem for lazy extraction

2. Branch by Abstraction

Idea: Build the new service inside the monolith first.

Example:

# Old way
class Order < ApplicationRecord
  def process_payment
    Stripe::Charge.create(...) # Direct call
  end
end

# New way
class Order < ApplicationRecord
  def process_payment
    PaymentService.new.process(order: self) # Wrapped call
  end
end

# Later, move PaymentService to its own app
Enter fullscreen mode Exit fullscreen mode

3. Event-Driven Decoupling

Idea: Use events to phase out direct calls.

Before:

# Monolith
class OrderController
  def create
    @order = Order.create!(...)
    InventoryService.update!(@order) # Direct call → tight coupling
  end
end
Enter fullscreen mode Exit fullscreen mode

After:

# Monolith
class OrderController
  def create
    @order = Order.create!(...)
    EventStore.publish(OrderCreated.new(@order)) # Loose coupling
  end
end

# Inventory Service (separate app)
class OrderCreatedHandler
  def call(event)
    Inventory.update!(event.order_id)
  end
end
Enter fullscreen mode Exit fullscreen mode

Tools:

  • Rails Event Store
  • Karafka (Kafka)

When NOT to Split

🚫 "Because microservices are cool": Complexity ≠ maturity.
🚫 If you lack DevOps muscle: No container orchestration? Think twice.
🚫 For trivial domains: A 10-model SaaS app isn’t Shopify.

Rule of Thumb:

"Extract when the pain of splitting is less than the pain of staying."


The Extraction Playbook

  1. Start with observability:

    • Trace cross-service calls with OpenTelemetry.
    • Log context boundaries (tagged_logger).
  2. Extract the easiest service first:

    • Low-risk, high-independence (e.g., "Email Service").
  3. Keep the monolith as the "orchestrator":

    • Let it handle auth, routing, and UI.
  4. Test aggressively:

    • Contract tests (Pact) for service boundaries.
    • Chaos engineering (Gremlin) for resilience.

"But we’re not Netflix!"

You don’t need to be. Even extracting one service (e.g., Payments) can:

  • Speed up deploys for other teams.
  • Isolate failures (no more checkout crashes because email broke).
  • Unlock tech flexibility (rewrite the service in Go if needed).

Have you survived a monolith split? Share the war story below.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.