Reliable integration

How to synchronize data between two systems

The goal is not to send everything everywhere. First, clearly decide which system owns each piece of data.

25 minutes · API, webhook, and queue

First, the short version

What is data synchronization?

Synchronization means two systems use the same important information — for example, an online shop and stock levels from ERP. The transfer usually goes through an API.

Do not aim for perfect equality every millisecond. Define a source of truth, an event flow, and a way to safely catch up delayed or missing data.

Get ready

What you need

Agree on the rules on paper before writing code.

  • A list of synchronized fields and the system that is the source of truth for each one.
  • Access to both systems’ APIs, test data, and credentials stored safely.
  • A public HTTPS URL if you want to receive webhooks.
  • A message queue or a job table. It lets synchronization finish even after a short outage on the other side.

Step 1

Design the flow before the first request

Start with one direction and one data type. Bidirectional synchronization without rules quickly creates conflicts.

1. Define the source of truth and shared key

  1. Write down the owner of every field. For example, ERP owns the price and the shop owns the product description.
  2. Store a stable external ID. A name or email can change and is not a reliable key.
  3. When there is a conflict, prefer the owner. Do not try to automatically merge two different edits.
external_id = "erp-product-1842"
RFC 9110: HTTP semantics

2. Use events and a catch-up check

  1. When the other system supports it, receive a webhook on a change. Process it quickly and hand the work off outside the HTTP response.
  2. Add periodic polling as a safety net. For example, fetch changes since the last synchronization once an hour.
  3. Never assume a webhook arrives exactly once. It can be delayed or delivered twice.
GET /products?updated_since=2026-08-10T09:00:00Z
Symfony Messenger

3. Process the change on a queue

  1. Put the record ID and version on the message queue, not a whole object that can become stale.
  2. The worker loads current data, checks the version, and writes the change to the target system.
  3. Store the result, time, and event ID. A unique ID gives you idempotence.
php bin/console messenger:consume async -vv
Symfony Messenger: queued messages

Step 2

Check the synchronization

Test a normal change and the situations that really happen in production.

  1. Change one record at the owner

    Check that the right field appears in the target system and the external ID stays unchanged.

  2. Send the same event twice

    The result must match the first attempt. This checks idempotence.

  3. Simulate an unavailable API

    The job must not disappear. It should remain on the queue or in failed transport with an actionable error.

    php bin/console messenger:failed:show

If something goes wrong

Common problems

Data overwrites itself back and forth

A field has no owner. Stop bidirectional writes and set a source of truth.

A webhook arrives more than once

Store an event ID with a unique constraint. Repeats then make no second change thanks to idempotence.

Records differ after an outage

Run catch-up polling from the last successful synchronization. Do not derive that time from the other system server clock.

A long import blocks the request

The endpoint should only accept work and hand it to a message queue. Keep synchronous imports for small manual actions.

Done

Your synchronization has clear rules.

Your API now has clear data ownership, safe repeats, and an outage catch-up check.

Request a call

I will call you on the next working day between 9:00 and 17:00.

You can also call me directly.

+420 605 181 728

Leave your phone number and send a callback request.

By sending, you agree to processing your data in order to handle your request.