In the context of the Strangler Fig architecture diagram, CDC (Change Data Capture) is the mechanism used to keep the new microservices’ databases synchronized with the legacy database in real-time without tightly coupling the systems.
When you break a monolith apart, the new services (like Catalog or Orders) need access to data that still lives in the legacy database. Instead of having the legacy application manually write to both databases (which is fragile and prone to data drift if one write fails), CDC listens directly to the database layer.
How CDC Works Step-by-Step
- Transaction Log Tapping (WAL): Databases like PostgreSQL record every single insert, update, and delete into a low-level transaction log (known as the Write-Ahead Log or WAL in PostgreSQL). CDC tools attach directly to this log stream. Because they read the native logs, they add virtually zero performance overhead to the running legacy application.
- Capturing Changes Asynchronously: When a user updates a record in the legacy monolith, the change is committed to the legacy database. The CDC tool instantly captures that delta (e.g., “Row X in table Users was updated”).
- Streaming via Event Broker: The captured change is serialized into a lightweight event message (JSON or Avro) and pushed to a streaming pipeline or broker (such as Kafka, Debezium, or AWS DMS).
- Updating the New Platform Databases: The new microservices consume these events from the broker and apply the corresponding updates to their own isolated Service databases.
Why Use CDC During a Migration?
- Zero Dual-Write Risk: If the legacy application had to write to both databases synchronously inside application code, a network failure or timeout on the new database would crash the main transaction. CDC decouples this entirely.
- Low Latency: Changes are streamed in milliseconds, meaning the new microservices have a near real-time replica of the data they need.
- Safe Decoupling: It allows the new services to read from their own fast, optimized database schemas while the legacy monolith finishes its gradual deprecation.
Data mapping and transformation layer for CDC
Because the legacy monolith’s database schema is usually messy, denormalized, or tightly coupled, the new microservices in the New platform will have completely different, cleaner database schemas. A column named usr_nm in an old legacy table might need to map to first_name and last_name in a new users table.
Where Does the Mapping and Transformation Logic Live?
Depending on how complex the transformation is, you have two primary architectural locations to place this logic:
Option 1: At the CDC Pipeline Level (Infrastructure Layer)
If the transformation is simple (e.g., renaming columns, dropping sensitive fields, changing data types), you handle it right inside the CDC streaming configuration.
- Tools: Debezium (using Single Message Transforms – SMTs) or AWS DMS mapping rules.
- How it works: The CDC tool intercepts the legacy database’s Write-Ahead Log (WAL), transforms the payload on the fly (e.g., renaming
old_coltonew_col), and pushes the already-mapped JSON directly to the new database or an intermediate topic. - Pros/Cons: Fast and automatic, but terrible for complex business logic, lookups, or relational restructuring.
Option 2: In a Dedicated PHP Consumer Worker (Application Layer — Recommended)
For a software architect, this is usually the cleaner and more maintainable approach—especially since you are using PHP.
- How it works:
- The CDC tool (or a tool like Debezium / Kafka Connect) captures the raw change event from the legacy database WAL.
- It drops that raw event into a message broker topic (e.g., Kafka, RabbitMQ, or Redis Streams).
- You build a dedicated PHP background consumer (using Symfony Messenger or Laravel Queues) that listens to that topic.
- The PHP worker contains the Explicit Mapping and Domain Logic, transforms the legacy payload into the new domain model, and saves it to the new service database via Prisma/Eloquent/SQL.
What the PHP Mapping Logic Looks Like
If you place the logic in a PHP consumer worker, you can write clean, testable domain mapping classes rather than raw database triggers:
declare(strict_types=1);
namespace App\Domain\Migration;
class UserDataMapper
{
/**
* Maps a raw legacy database CDC payload to the new service schema.
*/
public function mapLegacyUserPayload(array $cdcEvent): array
{
$legacyRow = $cdcEvent['after'] ?? [];
// Handle structural differences between legacy and new schema
return [
'id' => $legacyRow['usr_id'] ?? null,
'email' => strtolower(trim($legacyRow['usr_email'] ?? '')),
'first_name' => $this->extractFirstName($legacyRow['usr_full_name'] ?? ''),
'last_name' => $this->extractLastName($legacyRow['usr_full_name'] ?? ''),
'is_active' => (bool) ($legacyRow['status_flag'] === 1),
'migrated_at' => new \DateTimeImmutable(),
];
}
private function extractFirstName(string $fullName): string
{
$parts = explode(' ', trim($fullName));
return $parts[0] ?? '';
}
private function extractLastName(string $fullName): string
{
$parts = explode(' ', trim($fullName));
array_shift($parts);
return implode(' ', $parts);
}
}

