Skip to content
Skip article header Engineering

Legacy Data Migration Strategy

Google Cloud's own migration guidance states the constraint every cut-over plan has to accept: genuinely zero downtime is impossible. This guide works through the decision a legacy data migration actually turns on, big bang against outbox-based trickle against log-based change data capture, why a dual write without a transactional outbox can silently diverge two databases, reconciliation tiered from row counts to field-level checks and the rehearsal and rollback discipline AWS's own cutover guidance describes.

Updated 23 min read 41 views
A migration team in a cutover war room watching a replication-lag dashboard, one engineer holding a phone ready to call a rollback.
Skip key takeaways
  • Zero downtime is not achievable, only minimizable Google Cloud's own migration guidance states plainly that genuinely zero downtime is impossible, so every sync method here is a design for how small the unavoidable gap gets, not a way to remove it.
  • Dual write without a transactional outbox can silently diverge two databases A plain dual write has no shared transaction: a crash between the two writes, a timeout with an unknown outcome, a swallowed error or concurrent writers applying changes in different orders can all leave the databases silently disagreeing. The transactional outbox pattern replaces the second write with a relayed event the target applies idempotently, closing that gap for any write that goes through the outbox-writing code. A write that bypasses it still needs log-based CDC or reconciliation to be caught.
  • Reconciliation is tiered, not a single pass Row-level checks inherit any bug baked into the shared transform they compare against. A business aggregate computed independently in the legacy system does not. Field-level checks catch nothing a full-row checksum would miss, but financial tables use them anyway for diagnosability: they name the exact field that disagrees.
  • Phased cut-over rollback wins on blast radius, not on being the only routing change Provided the two-way database sync feeding it is already running, either a phased or an all-at-once cut-over can roll back with a routing change. Phased's real advantage is that only the portion of traffic already cut over has to revert, not the whole solution at once.
  • CDC lag becomes a failure mode when cut-over trusts a lag metric, not a confirmed log position A connector can run cleanly for weeks and still be behind the source, so cutting over on a lag number instead of a log position confirmed after the source stops taking writes goes live on a quietly incomplete target.

A legacy data migration strategy is pitched as a schema-mapping problem: transform the old columns into the new ones, run a script, done. The part that actually breaks a cut-over sits one layer under that, in how long the source and the target are allowed to disagree with each other and what happens the moment they do. Google Cloud's own migration guidance names the constraint that forces every design decision here: "In a migration, achieving truly zero downtime for clients is impossible; there are times when clients cannot process requests." Every near-zero-downtime cut-over is minimizing that gap, not eliminating it, and the sync method chosen decides how small the gap gets and what it costs to keep both databases honest while it is open.

In short: the decision table below holds the three sync methods a cut-over actually chooses between, what each one costs and the failure mode it produces. The plan has to defend against each failure mode with a specific check: dual writes without an outbox can silently diverge the two databases because there is no shared transaction spanning them, and skipping the reconciliation gate before cut-over lets a drained-but-unverified target go live.

Inventory and profiling before any sync method is chosen

Before a sync method is picked, the legacy schema itself has to be read, not assumed - general engineering discipline rather than a sourced standard for this guide. Inventory means a table-by-table pass that records, for every table in scope, its row count, its primary key strategy (a real auto-increment column, a natural key, a composite key or no reliable key at all), every foreign key pointing in and out of it and every column whose meaning is not obvious from its name. Legacy schemas accumulate columns nobody remembers the purpose of, and a migration that maps every column blind carries that ambiguity into the new system instead of resolving it once. Profiling is the companion pass: the actual distribution of values in production data, not the type declared in the schema. A column typed as NOT NULL in the DDL can still be full of empty strings a downstream system treats as null. A column typed as a normal integer can carry values that overflow the target type's range. A date column can carry sentinel values such as 0000-00-00 or 9999-12-31 that a naive transform silently accepts and a business rule silently mishandles later. None of this shows up by reading the schema. It shows up by querying the data.

The output of this stage is not a migration script. It is a list of open questions against the business: which columns are safe to drop, which values need an explicit mapping rule before a single row moves and which tables carry a key structure fragile enough that the sync method itself has to account for it. The identity and encoding failure modes covered later on are inventory gaps that were never closed before the sync method was built around an assumption that turned out to be wrong. Where the target system is itself being decomposed into services rather than kept as one database, our microservices architecture development team scopes which service owns which table before the sync method is chosen, because a foreign key that used to sit inside a single database now has to cross a service boundary instead.

Sync-method decision: big bang, outbox-based trickle and log-based CDC

Approach When it fits What it costs you Reconciliation needed Failure mode seen in practice
Big bang with downtime A downtime window is available and the dataset is small enough to move inside it A scheduled outage, and every hour of copy time is an hour the business is not open The same reconciliation tiers as any other approach, run once after the copy completes and before traffic returns The copy runs longer than the window and the team either extends an announced outage or cuts over on an unverified partial copy
Outbox-based trickle Application code can be changed to write a local outbox row in the same transaction, and the target must stay current while the source serves traffic A local business change plus an outbox event in one transaction, a relay and an idempotent apply on the target Continuous reconciliation for the whole dual-write window, not a single pass at the end, plus a cut-over step to stop source writes and confirm the relay has applied everything through the last committed outbox id A write path that skips the outbox, or an apply that is not idempotent or lands out of order
Log-based CDC The application cannot tolerate the failure coupling and added write latency dual-write code introduces, the application code cannot be safely changed to write twice or writes bypass the application entirely (batch jobs, stored procedures, other applications) where an outbox would never see them A connector against the source transaction log, access to that log granted and monitored and lag against the source tracked continuously A drain step before cut-over confirms no unreplicated change remains, then the same tiered reconciliation as any other approach Replication lag at the exact moment of cut-over means the target is behind the source by an unknown amount and the switch happens before the drain actually completes

Three techniques exist for keeping a target database current with a source database during a cut-over window, and the choice between them is really about how much downtime the business will tolerate against how much engineering effort the team spends keeping two databases honest with each other - the trade-off our legacy modernization services team weighs against the real downtime tolerance and write volume rather than whichever sync method is easiest to prototype. None of the three actually reaches zero downtime: as Google Cloud's own migration guidance puts it, "In a migration, achieving truly zero downtime for clients is impossible; there are times when clients cannot process requests," so each technique below is minimizing that gap rather than closing it. AWS Prescriptive Guidance's own cutover strategy documentation frames this dependency directly: "The database cutover strategy is usually tightly coupled with the downtime requirements for the application." The narrowest-effort option is a one-time, offline move under a locked source. A big-bang migration accepts a downtime window, locks the source, copies everything once and switches over. It is the simplest sync method to reason about because there is no window in which the two databases can drift, but the downtime it requires is not always available to the business.

An outbox-based trickle keeps the target current while the source stays live and serves traffic, replacing the naive form of dual write that AWS names for keeping an active-active pair synchronized: "You can either perform dual write operations so that changes are made to both databases", against the alternative of a bi-directional replication tool the same guidance names in the same sentence. The trickle itself moves data one way, from source to target, before the target is promoted to serve traffic on its own, though the residual downtime gap this guide opened with still applies. The cost, matching the decision table above, is a local business change plus an outbox event in one transaction, a relay and an idempotent apply on the target: because a dual write like that is not a single transaction spanning both databases, this guide's trickle method replaces the second write with a relayed outbox event rather than issuing two naive writes.

The third technique relies on reading the source database's own transaction log rather than writing to two places at once. AWS's cutover PDF names this dependency for its flash-cut strategy directly: "This strategy relies on continuous data replication (CDC) from the source database to the target database." Debezium's own architecture documentation, cited here as documentation of the technique and not as a product recommendation, describes the mechanism per engine: "The MySQL connector uses a client library for accessing the binlog", while the PostgreSQL connector reads from its own logical replication stream instead. Log-based CDC never touches application code or adds work to the source write path, unlike trigger-based CDC, whose triggers live in the database and add a schema change plus work inside every source transaction, which is exactly what makes the log-based approach lower-risk than dual write and exactly why it costs more to set up: a connector has to be configured, granted access to the transaction log and monitored for lag against the source, the source itself often needs a setting changed to expose that log in the first place, row-based binlog format for MySQL or wal_level=logical for PostgreSQL - a setting that adds no statements to source transactions, only extra log volume - and a stalled PostgreSQL replication slot keeps retaining WAL until the source disk fills - none of which a big-bang or naive dual-write migration requires. For either continuous method (outbox or log-based CDC), the initial bulk load that seeds the target still has to be pinned to a specific log position or outbox offset, so writes that land during that load are neither lost nor applied twice once the ongoing sync catches up from that same point.

Same-engine migrations, MySQL to MySQL or PostgreSQL to PostgreSQL, keep the CDC connector straightforward because source and target already share a driver and a wire protocol, but that alone does not guarantee the reconciliation checksums line up: even a same-engine migration can differ across versions, such as the default collation for the utf8mb4 character set changing from utf8mb4_general_ci to utf8mb4_0900_ai_ci in MySQL 8.0, which changes how two otherwise-identical strings compare and sort without changing the bytes stored: the same pair of strings, differing only in case, accents or trailing spaces under PAD SPACE versus NO PAD, can compare and sort differently depending on which collation is in effect. A schema remap needs a normalized projection even on a single engine whenever a difference like that sits between source and target. Cross-engine migrations need that same projection for a different reason: a connector has to exist for both engines, and a checksum on raw column values will not match across engines unless both sides are normalized onto the same projection first, the same normalization the field-level tier depends on.

The dual-write consistency problem and the transactional outbox

A dual write is two separate writes to two separate databases, and nothing about that guarantees either atomicity or ordering between them. Nothing about a naive dual write surfaces its own divergence: causes include a crash between the two writes, a timeout that leaves the outcome unknown, a swallowed or blindly retried error, two concurrent writers whose changes land in a different order on each side or a write path that bypasses the dual-write code entirely, such as a batch job, a stored procedure or another application writing to the legacy database directly. Any one of these leaves the databases disagreeing with nothing in a naive implementation to detect it. The Parallel Change entry on martinfowler.com, by Danilo Sato, names the structure of the window this problem lives in, describing a pattern written about interface changes generally: it "is a pattern to implement backward-incompatible changes to an interface in a safe manner, by breaking the change into three distinct phases: expand, migrate, and contract." The same three-phase structure maps onto a database cut-over, even though Sato's own examples describe a schema change inside a single database rather than a migration across two: "Most database refactorings follow the parallel change pattern, where the migrate phase is the transition period between the original and the new schema, until all database access code has been updated to work with the new schema."

Documented by Debezium as a technique rather than pitched as a product, the transactional outbox is the pattern that closes the silent-divergence gap without a distributed transaction. "The outbox pattern is a way to safely and reliably exchange data between multiple (micro) services." The outbox does not make the two databases atomic, and it does not eliminate the gap between the source write and the target write. What it does is replace the second write: the application writes its business change and an event record together, in one local transaction inside the source database, so the two either both commit or both roll back. A separate relay then delivers that event to the target at least once, and the target applies it idempotently and in order per key: the target can still lag behind the source, but it cannot silently miss a change that goes through the outbox-writing code. Any write that bypasses that code, such as a batch job, a stored procedure or another application writing to the source directly, is invisible to the outbox guarantee, and catching it needs log-based CDC or a reconciliation pass instead. Debezium states the guarantee this buys directly: "An outbox pattern implementation avoids inconsistencies between a service’s internal state (as typically persisted in its database) and state in events consumed by services that need the same data." The relay can be a log reader (the way Debezium's own outbox event router is) or a poller that queries the outbox table directly instead. When it is implemented as a change-data-capture transformation, the way Debezium's own outbox event router is, an outbox-based trickle inherits the same connector and replication-lag costs log-based CDC carries on its own. A dual-write migration that skips the outbox and simply issues two writes from application code is choosing to reintroduce the exact window the pattern exists to close.

Reconciliation tiers

Layered reconciliation reports, a row-count summary and a field-level diff, stacked on a migration engineer's desk.

Reconciliation is not a single check run once at the end. It is a tiered set of checks that increase in cost and diagnostic detail as confidence in the target increases, and none of the sources consulted for this guide specify a numeric tolerance, a checksum algorithm or a named financial-industry reconciliation standard for any of them, so the tiers below are stated as increasing in cost and diagnostic detail, not as a fixed threshold to hit.

A row count per table is the cheapest check available, catching the crudest failure: rows dropped, duplicated or never copied at all. A checksum pass follows, comparing a computed hash of each row between source and target, which catches a row that exists in both places but disagrees on content - though a hash only means anything once both sides are hashed over the same normalized projection, since numeric precision, timestamp precision and encoding can all change the stored bytes across database engines by default; collation does not change the stored bytes, but it changes how those bytes compare and sort, which still breaks a reconciliation step that orders or groups rows before comparing them. Row-level checks, whether a checksum or a field-by-field comparison, compare the target against that same transformed projection, so they inherit any bug baked into the transform itself: if the transform silently miscalculates a value, a check that compares the miscalculated value to itself will not catch it. Business-aggregate checks close that gap: a sum, a count or a balance computed independently in the legacy system before the transform runs, then compared against the same aggregate in the target, is not exposed to a transform bug the row-level tiers share, and it catches transform bugs that change totals - though it is not exhaustive, since rows misassigned to the wrong record while the overall sum stays the same still pass an aggregate check untouched. Field-level reconciliation, reserved for financial tables, compares every field on every row explicitly rather than relying on a row-level checksum, and it catches nothing a full-row checksum over the same normalized projection would miss. It earns its added cost on diagnosability alone: it names the exact field that disagrees rather than just flagging that the row does. Financial tables, the ones carrying account balances or transaction history, are exactly where this fourth tier stops being optional - the same tables our banking software development practice is built around. The gate itself is qualitative: every difference the reconciliation surfaces is either zero or explained and signed off, never absorbed into an invented tolerance.

Reconciliation itself is not a one-time event even under log-based CDC. Google Cloud's own migration vocabulary names the drain step immediately before cut-over: "you must migrate remaining changes from the source databases to the target databases" - what that guidance calls draining. Watching lag metrics alone is not enough: the procedure has to stop writes on the source, record its own log position at that moment and confirm the connector has applied everything up to that position before cut-over proceeds. The same discipline binds an outbox-based trickle: cut-over does not proceed until the team can stop source writes and confirm the relay has applied everything through the last committed outbox id. Microsoft's Azure architecture guidance names a related check as an explicit precondition: "Validate consistency between both databases before cutover." A reconciliation failure caught at this stage is not automatically a hard-abort trigger. It is something a team investigates and fixes, as one account of event interception reported mid-migration describes: "Where there were issues with reconciliation checks, the team could reason about, and fix them ensuring consistency was achieved - without business impact." The gate is what matters, not treating every mismatch as a full stop: cut-over does not proceed until the drain is confirmed and the checks pass, but a caught and explained mismatch is the reconciliation process working, not failing.

Rehearsals

AWS's own pre-cutover planning guidance lists rehearsal among the steps a team can take to reduce cut-over risk: "To reduce the risk of delay, rework, unplanned outages, data loss, performance issues, and a poor user experience, you can develop a cutover plan, create a cutover workbook, and practice rehearsing your cutover." A rehearsal against a realistic copy of production data surfaces the gaps an inventory pass and a reconciliation script did not catch on paper: a migration step that takes longer against real data volume than it did against a test dataset, a reconciliation check that produces false positives against real-world data quality, a rollback step that was never actually exercised end to end.

Rehearsals also establish the precondition for the cut-over window itself. When the data cannot be allowed to change mid-flight, which is the case for a big-bang approach and for the final drain of a CDC-based or outbox-based one - stop source writes and confirm the relay has applied everything through the last committed outbox id for the outbox case or the connector through the confirmed log position for the CDC case - AWS's guidance frames locking the source as something the team may need to do: "then you may need to lock the source environment (such as a database lock) prior to starting the cutover process." A rehearsal is where the team confirms that lock actually stops writes cleanly, rather than discovering a write path that bypasses it during the real cut-over.

Cut-over patterns as AWS Prescriptive Guidance names them

AWS Prescriptive Guidance names cut-over patterns at two different layers, and the two are separate decisions, not one. At the database layer, the strategy database migration guide names four patterns: offline migration, flash-cut migration, active-active database configuration and incremental migration. Mapped onto the sync methods this guide covers: offline migration is the big-bang approach above, flash-cut migration is a short traffic switch fed by log-based CDC and active-active configuration is kept synchronized by either dual write or two-way replication. Incremental migration, moving the system in parts rather than in one cut-over, is out of scope here and belongs to our legacy modernization guide.

At the application and traffic layer, a separate AWS Prescriptive Guidance document names two distinct approaches to how traffic itself gets redirected once the data side is ready. The first is all-at-once: "If you take the all-at-once approach, then you cut over the entire solution with a flip of a switch." The second is phased, where traffic shifts gradually across a mix of migrated and unmigrated servers rather than switching everything simultaneously. These are not competing choices between one data-layer pattern and one traffic-layer pattern. A team running log-based CDC at the data layer can still choose all-at-once or phased traffic cut-over on top of it, because the two decisions answer different questions: how the data gets synchronized, and how client traffic gets pointed at wherever that data now lives.

Rollback triggers and sign-off

A rollback path has to be designed before cut-over starts, not improvised after something goes wrong. AWS's own guidance states this as a planning obligation: "Be sure to document a rollback procedure as part of the cutover plan." Sometimes the source system stays live and untouched as a fallback until the target has proven itself. Google Cloud's own migration guidance names exactly this as one of the components a migration architecture needs: "A setup architecture that supports a fallback if unforeseen errors occur during a migration." The same guidance names the practical reason a team reaches for that fallback: "Sometimes you keep the source databases as a fallback measure if you encounter unforeseen issues with the target databases." A rollback trigger is best defined ahead of cut-over as a short, specific list: a reconciliation tier that fails and cannot be explained within an agreed window, an application error rate above the pre-cut-over baseline or a business-critical query returning materially wrong results. Naming the triggers in advance is what keeps a rollback decision from being made under pressure, mid-incident, by whoever happens to be on call.

The source-as-fallback design has a point of no return. Once the target has accepted writes of its own, an untouched source is stale the moment those writes land, and getting back to a cheap rollback again requires either the active-active two-way sync above already running or an agreed acceptance of the loss window between the cut-over and the rollback decision. That reverse replication has to be running from the target's first write, not started only once a rollback is needed, or the gap between cut-over and the rollback decision stays unreplicated regardless. A phased traffic cut-over that sends writes to both sides during the transition needs that same two-way synchronization going forward, not just the one-way sync that got the target current in the first place.

Provided the two-way sync above is already running, a routing change is enough to roll back either a phased or an all-at-once cut-over, because the source has stayed current with the target's writes throughout and no second full data move is needed. What still separates the two is blast radius: a phased rollback only has to revert the portion of traffic already cut over, while an all-at-once rollback has to revert everything at once. Without reverse replication running from the target's first write, both face the same outcome: an accepted loss window. AWS's own guidance describes the phased mechanism at the application-server layer, not the database state: "Because you have a mix of migrated and existing servers that serve the application with a load spread between them, it is both fast and simple to revert back in the event of issues." Sign-off is not a single moment at cut-over either. AWS's post-cutover guidance describes a common warranty period, a bounded window of continued accountability after the switch completes during which the migration team remains responsible for issues that surface once real production traffic has hit the new system: "It's common for migration projects to have a warranty period in which the migration teams provide support in the event that an issue occurs within a predefined window (typically from one day to one week)." That range is explicitly a typical span, not a fixed number to copy into every project plan, and the length that fits a given migration depends on its own risk profile.

Failure modes to plan for

Sequence and identity collisions surface when a target database's auto-increment or identity column starts counting from its own default rather than being seeded past the highest value the migration copied in: the next row written after cut-over collides with an existing row, or worse, silently reuses an identity value a different legacy record used to carry. MySQL's InnoDB engine moves its AUTO_INCREMENT counter past any explicitly inserted value automatically, while PostgreSQL and Oracle sequences do not: both need an explicit reseed, setval or the equivalent, after the copy or the same collision follows. Time zone and encoding drift is an inventory gap rather than a migration-tool bug: a source column stored naive local time gets copied into a target column that assumes UTC, and every timestamp is now wrong by an offset tied to whichever time zone was in effect when the row was written. Soft deletes are a related trap: a row flagged deleted in the source but still physically present is easy to migrate faithfully and easy to then treat as live in the target if the deletion flag was never part of the inventory pass. Orphaned references appear when a foreign key in the source points at a row already missing before migration started, silently tolerated by the legacy application's own code and then surfaced as a hard constraint violation once the target schema enforces referential integrity properly. CDC lag at cut-over becomes a failure mode when the team cuts over on a lag metric instead of a confirmed log position: a connector that has run cleanly for weeks can still be seconds or minutes behind the source, and a small lag number alone does not confirm the connector has applied everything up to the log position recorded when the source stopped taking writes. A cut-over that proceeds on the lag metric instead of that confirmed position goes live on a target that is quietly incomplete. Skipping the reconciliation gate makes every other mode worse: treating the tiers as optional, or running only the cheapest one, cuts over on a target whose divergence from the source was never actually measured.

How Pharos Production helps

A legacy data migration can be one step inside a wider modernization effort, and the decision of whether and how to modernize the legacy system itself is a separate question from the cut-over procedure this guide covers; our legacy modernization guide covers that strategic layer, including when a strangler-style incremental replacement fits better than a single cut-over. Where the target architecture on the other side of the migration is itself being decomposed rather than replaced wholesale, our microservices vs monolith guide covers that architectural decision.

Our legacy modernization services team runs cut-overs end to end, from sync-method selection through the outbox pattern and the reconciliation gate. Our microservices architecture development team scopes data ownership when the target is being decomposed, and our cloud services and migration team handles target-side provisioning when the destination is a managed cloud database.

Sources: Google Cloud architecture guidance on docs.cloud.google.com; AWS Prescriptive Guidance on docs.aws.amazon.com (the strategy database migration PDF) and its cutover best-practices pages (pre-cutover, cutover and post-cutover stages); the Parallel Change entry on martinfowler.com (Danilo Sato) and Patterns of Legacy Displacement on martinfowler.com (Ian Cartwright, Rob Horn and James Lewis; event interception); Debezium documentation on debezium.io (architecture and the outbox event router), cited as documentation of the change data capture and outbox techniques, not as a product recommendation; Microsoft's Azure architecture guidance on learn.microsoft.com. Read 23 September 2026. Engineering guidance, not a specific migration plan for any given system.

FAQ

Last updated:

Quick answers to common questions about custom software development, pricing, process and technology.

  • Copy link Copies a direct link to this answer to your clipboard.

    No, and Google Cloud's own migration guidance says so directly: achieving truly zero downtime for clients is impossible, because there are times when clients cannot process requests during a migration. Every design described as near-zero-downtime, including log-based change data capture, is minimizing that unavoidable gap rather than eliminating it.

    Planning for a residual gap, however small, is more honest and more useful than designing around a zero-downtime promise no migration actually delivers.

  • Copy link Copies a direct link to this answer to your clipboard.

    Because a plain dual write is two separate writes to two separate databases with no shared transaction, so a crash between the two writes, a timeout with an unknown outcome, a swallowed or retried error or two concurrent writers applying changes in a different order on each side can all leave the databases disagreeing silently. The transactional outbox pattern does not make the two writes atomic.

    It replaces the second write. In the source database, the business change and an event row commit together. A relay then delivers that event to the target at least once, and the target applies it idempotently and in order per key. The target can still lag behind the source, but it cannot silently miss a change that was actually committed - as long as that change went through the outbox-writing code. A write that bypasses it, such as a batch job, a stored procedure or another application writing to the source directly, still needs log-based CDC or reconciliation to be caught.

  • Copy link Copies a direct link to this answer to your clipboard.

    Because a checksum is only comparable when it is computed over the same normalized projection on both sides. Numeric precision, timestamp precision and encoding differ across database engines by default and can produce different bytes when hashed raw.

    A collation difference is different again, changing how two values compare and sort rather than the bytes stored, which is still enough to break a reconciliation pass that relies on collation-sensitive ordering or grouping. A reconciliation pass has to normalize both sides onto the same projection before hashing, and treat the result as a qualitative gate: every difference is either zero or has been explained and signed off, never absorbed into an invented tolerance.

  • Copy link Copies a direct link to this answer to your clipboard.

    It gets expensive fast. Once the target has accepted writes of its own, the source is stale the moment those writes land, and a cheap rollback by simply pointing traffic back is no longer available.

    This is why the rollback trigger list has to be agreed before cut-over starts rather than worked out mid-incident. Unless reverse replication has been running since the target's first write, the source is stale, and the only way back to a rollback-ready state is an explicit decision to accept the loss window between cut-over and the rollback.

  • Copy link Copies a direct link to this answer to your clipboard.

    An idempotent target means applying the same event twice produces the same end state as applying it once, which matters because the outbox relay guarantees at-least-once delivery, not exactly-once. A relay that retries after a timeout, or that redelivers after a crash before acknowledging, can send the same event more than once.

    Reapplying a set-state upsert, one that writes a field to an explicit value rather than adjusting it, is idempotent: applying it twice lands on the same end state as applying it once. That alone is not the whole guard, though, because idempotency says nothing about an event's age: a set-state upsert delivered out of order can still overwrite a newer value with an older one, so it still needs the version check against a stale redelivery. The hazard is sharper for a non-idempotent operation such as an increment, where a redelivered event applies the change twice. The fix for both is the same: a per-key version number or log-position check that lets the target recognize and skip an event it has already applied, or one older than what it already holds.

  • Copy link Copies a direct link to this answer to your clipboard.

    There is no fixed number, and treating one figure as universal misreads the source it is drawn from. AWS's own post-cutover guidance describes a typical warranty period as ranging from one day to one week, stated explicitly as a range rather than a fixed value, and the length that fits a given migration depends on its own risk profile, the reconciliation tiers it required and how business-critical the migrated data is.

I work with startup founders who need a dedicated software development team but don’t want to gamble on hiring, random outsourcing, or opaque delivery.
Most founders face the same problem sooner or later.
Early technical and team decisions lock the product into tech debt, slow delivery, missed milestones and constant re-hiring. By the time this becomes visible, fixing it is already expensive.

As a CTO and software architect, I help founders design, build and run dedicated development teams that work as a true extension of the startup. Not as a black-box vendor.

My focus is on complex products where mistakes are costly:

  • Web3 and blockchain platforms
  • FinTech and regulated products
  • High-load startup systems
  • MVP → scale transitions

We don’t do body-shopping.
We don’t sell generic outsourcing.

Instead, we help founders:

  • build the right team structure from day one
  • keep technical ownership and transparency
  • scale delivery without losing control
  • avoid vendor lock-in and hidden risks

Teams are aligned with the product roadmap, business goals and long-term architecture. Not just short-term velocity.

Dmytro Nasyrov, Founder and CTO at Pharos Production
Dmytro Nasyrov Founder & CTO Let's work together!

Your business results matter

Achieve them with minimized risk through our bespoke innovation capabilities

Your contact details
Please enter your name
Please enter a valid email address
Please enter your message
* required

We typically reply within 4 hours

What happens next?

  1. Contact us

    Contact us today to discuss your project. We're ready to review your request promptly and guide you on the best next steps for collaboration

    Same day
  2. NDA

    We're committed to keeping your information confidential, so we'll sign a Non-Disclosure Agreement

    1 day
  3. Plan the Goals

    After we chat about your goals and needs, we'll craft a comprehensive proposal detailing the project scope, team, timeline and budget

    3-5 days
  4. Finalize the Details

    Let's connect on Google Meet to go through the proposal and confirm all the details together!

    1-2 days
  5. Sign the Contract

    As soon as the contract is signed, our dedicated team will jump into action on your project!

    Same day