"Your CRUD app is a ticking time bomb. Here’s how to defuse it—without a rewrite."
Legacy systems are like old houses:
- Every change risks breaking something
- No one remembers why the plumbing works this way
- Adding a new room feels impossible
But migrating to event sourcing doesn’t require burning it all down.
Here’s how we moved a 500-table Rails monolith to event sourcing—one component at a time—without downtime.
Phase 1: Lay the Foundation
Step 1: Identify the Pain Points
Start with the highest-value, highest-pain areas:
- Audit-heavy domains (payments, user access)
- Complex workflows (order fulfillment)
- Frequent "how did this happen?" debugging
Avoid: Static reference data (e.g., product catalogs).
Step 2: Add Event Publishing to Legacy Code
# Wrap existing updates in events
class Order < ApplicationRecord
after_update :publish_events
def publish_events
if status_previously_changed?
EventPublisher.publish(
OrderStatusChanged.new(
order_id: id,
old_status: status_previous_change[0],
new_status: status
)
)
end
end
end
Why? Starts building an event history without changing behavior.
Phase 2: Dual-Write Architecture
Step 3: Build Parallel Projections
class OrderProjection
def self.for(order_id)
events = EventStore.for(order_id)
events.reduce({}) { |state, event| apply(state, event) }
end
def self.apply(state, event)
case event
when OrderStatusChanged
state.merge(status: event.new_status)
# ...
end
end
end
Test by comparing:
LegacyOrder.find(123).status == OrderProjection.for(123).status
Step 4: Shift Reads Gradually
- New features use projections
- Legacy code keeps using ActiveRecord
- Monitor for mismatches
Phase 3: The Big Flip
Step 5: Replace Writes with Commands
# Before
order.update!(status: "shipped")
# After
ShipOrderCommand.call(order_id: order.id)
# Emits OrderShipped event
Step 6: Retire Legacy Code
- Verify projections match 100% for 30 days
- Redirect all reads to projections
- Drop unused tables (celebrate 🎉)
Migration Pitfalls
🚨 Pitfall 1: Event schema changes
- Fix: Use upcasters to version events
🚨 Pitfall 2: Projection drift
- Fix: Rebuild from events + checksums
🚨 Pitfall 3: Performance regressions
- Fix: Add snapshots for hot aggregates
When to Abort
❌ Team lacks event-sourcing experience (train first!)
❌ No buy-in for incremental migration
❌ Leadership expects "2-week rewrite"
"But We Have 500 Models!"
So did we. Start with one:
- Pick a bounded context (e.g., payments)
- Prove value (better audits? faster debugging?)
- Expand iteratively
Have you survived a migration? Share your lessons below.
Top comments (0)