How to migrate databases from a monolith without downtime
Learn how to apply the Strangler Fig pattern to relational databases and ensure bidirectional synchronization during critical transitions to microservices.
Summary
- Migrating data from a monolith requires slicing the database without interrupting live system operations
- The Strangler Fig pattern focuses on isolating business domains gradually instead of rewriting everything at once
- Bidirectional synchronization using Change Data Capture prevents data loss during read and write transitions
- Two transition phases ensure the new microservice takes over the data flow with total safety
- Parallel consistency tests validate whether the new system responds identically to the legacy database
Why database migration is the biggest microservices challenge
When we decide to transform a monolithic system (a single application where all code and database live together) into microservices (small independent services), the biggest nightmare is not rewriting code, but separating data. In a monolith, different tables talk to each other through foreign keys and complex queries known as JOINs. In practice, this means breaking a giant database into smaller pieces without taking down production requires surgical precision.
Many teams try to make this switch all at once, turning off the system on Friday night and turning it back on Monday with the new architecture. This method, known as a big bang, almost always results in catastrophic failures, corrupted data, and hours of downtime that infuriate users. To avoid chaos, we must adopt incremental strategies that allow us to update the data infrastructure while the engine is running, ensuring business continuity. If you want to understand the fundamentals of this separation in legacy systems, check out our guide on how to decouple databases in legacy microservices before moving forward.
Applying the Strangler Fig pattern to data
The Strangler Fig pattern (inspired by a fig tree that wraps around a host tree until it replaces it) proposes replacing the old system gradually. When applied to databases, it prevents us from moving all tables at once. Instead, we choose a specific business domain—such as customer registration—and create a new database dedicated exclusively to it, while the rest of the monolith remains untouched on the legacy database.
In practice, this means building an intermediate routing layer that decides where each read or write request goes. If the application needs to query migrated customer data, it accesses the new service; if it needs products, it still queries the legacy monolith. This peaceful coexistence drastically reduces the risk of change, as any localized issue can be rolled back quickly without compromising the entire ecosystem.
Bidirectional synchronization with Change Data Capture
During the transition period, the old application and the new microservice must coexist operating on similar data, creating the critical challenge of consistency. If a customer updates their address in the old interface, that change must immediately reflect in the new database, and vice versa. To solve this without creating fragile coupling in the code, we use Change Data Capture, a technology that monitors the transaction log of the legacy database and replicates events in real time.
Tools like Debezium listen to every insert, update, or deletion made in the main database and publish these events to a message broker, such as Apache Kafka. The new microservice consumes these events and updates its own database asynchronously. This bidirectional flow ensures both ends stay synchronized, allowing the team to perform stress tests and performance validations without fear of losing critical information.
Step-by-step transition in safe phases
To execute the migration without downtime, we divide the process into controlled stages that avoid operational bottlenecks and protect the integrity of transactional information.
The first step involves setting up asynchronous replication of legacy data to the new base, ensuring the new microservice receives a mirrored and updated copy of all operational history.
The second step involves routing only read operations to the new service while keeping writes on the monolith for rigorous validation of the results delivered to the end user.
The third step performs the definitive flip of write operations, turning the new microservice into the primary source of truth for that specific business domain.
Handling conflicts and ensuring eventual consistency
Even with efficient synchronization, network delay (latency) can create small windows where data differs between the monolith and the microservice. In distributed architectures, we accept that consistency is not immediate everywhere, but it must converge quickly to a correct state. To mitigate this effect, we implement background reconciliation routines that check for discrepancies and fix eventual desynchronizations before they affect the user.
Furthermore, when unexpected slowdowns occur during this transition phase, it is vital to have observability tools to track where the bottleneck hides. If you want to dive deeper into monitoring performance failures during this phase, check out our guide on slow microservices and how to debug bottlenecks with OpenTelemetry. Identifying latencies early prevents database issues from spreading across the system.
Final thoughts on data migration
Migrating databases from a monolith to microservices without downtime is not purely a technical project, but an exercise in risk management and evolutionary architecture. The secret to success lies in slicing the problem into smaller pieces, keeping legacy systems running in parallel with the help of Change Data Capture, and rigorously validating every change before shutting down old structures. With planning and operational patience, the transition happens invisibly to the end user.