Menu

Data Integration And Consistency

Learn Data Integration And Consistency for API Engineer for free: roadmap, examples, subskills, and a skill exam.

Published: January 21, 2026 | Updated: January 21, 2026

Why this skill matters for API Engineers

APIs move data between services, databases, and external partners. Data Integration and Consistency ensures that information is transferred accurately, stored reliably, and stays correct even when systems fail, retry, or evolve. Mastery here unlocks trustworthy webhooks, safe upserts and merges, idempotent endpoints, robust background processing, and clean schema evolution across versions.

What you will be able to do

  • Design API endpoints and pipelines that are idempotent and safe under retries.
  • Choose appropriate transaction boundaries and understand their trade-offs.
  • Implement outbox and event-driven patterns for reliable async delivery.
  • Handle partial failures and reconcile data across systems.
  • Adopt eventual consistency where needed with clear SLAs and user messaging.
  • Evolve schemas without breaking existing clients.

Who this is for

  • Backend and API engineers shipping integrations, webhooks, or data syncs.
  • Engineers responsible for reliability of cross-service workflows.
  • Developers moving from monoliths to distributed systems and events.

Prerequisites

  • Comfortable with HTTP APIs (methods, status codes, headers).
  • Intermediate SQL (transactions, indexes, basic normalization) and at least one relational DB.
  • Basic familiarity with a server language (e.g., Go, Java, Node.js, Python).

Learning path

Step 1: Map domain data to storage. Practice upserts and merge logic. Learn how to prevent duplicates with unique keys and idempotency keys.
Step 2: Master transaction boundaries. Use single-DB transactions where possible; learn when to split into sagas or async steps.
Step 3: Handle partial failures. Implement retries with backoff, dead-letter queues, and reconciliation jobs.
Step 4: Add eventual consistency patterns. Build read models and projections with clear freshness guarantees.
Step 5: Use the Outbox pattern. Persist events atomically with business changes; deliver via a relay worker.
Step 6: Harden webhooks. Provide at-least-once delivery, signatures, and idempotency.
Step 7: Plan schema evolution. Use additive changes, defaults, and versioned payloads.

Worked examples (with code)

1) Idempotent create with Outbox pattern

Goal: ensure that a POST /payments endpoint can be safely retried and that an event is reliably emitted.

-- Tables
CREATE TABLE payments (
  id UUID PRIMARY KEY,
  external_id TEXT UNIQUE NOT NULL, -- idempotency key from client
  amount_cents INT NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending'
);

CREATE TABLE outbox (
  id UUID PRIMARY KEY,
  aggregate_type TEXT NOT NULL,
  aggregate_id UUID NOT NULL,
  event_type TEXT NOT NULL,
  payload JSONB NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  delivered_at TIMESTAMPTZ
);
// Pseudocode
function createPayment(req) {
  const key = req.headers['Idempotency-Key'];
  const body = req.body;

  db.transaction(() => {
    // Try insert; on conflict, return existing
    const payment = db.query(`
      INSERT INTO payments (id, external_id, amount_cents, status)
      VALUES (gen_random_uuid(), $1, $2, 'created')
      ON CONFLICT (external_id) DO UPDATE SET amount_cents = EXCLUDED.amount_cents
      RETURNING *;
    `, [key, body.amount_cents]);

    // Write outbox atomically with business change
    db.exec(`
      INSERT INTO outbox (id, aggregate_type, aggregate_id, event_type, payload)
      VALUES (gen_random_uuid(), 'payment', $1, 'PaymentCreated', $2);
    `, [payment.id, JSON.stringify(payment)]);
  });

  return {status: 201};
}

// A worker periodically relays undelivered outbox rows
function outboxRelay() {
  const rows = db.query(
    "SELECT * FROM outbox WHERE delivered_at IS NULL ORDER BY created_at LIMIT 100"
  );
  for (const row of rows) {
    try {
      http.post(EVENT_BUS_URL, row.payload);
      db.exec("UPDATE outbox SET delivered_at = now() WHERE id = $1", [row.id]);
    } catch (e) {
      // leave for retry; use exponential backoff in scheduling
    }
  }
}

Why it works: The business write and the event are in the same DB transaction. Retries are safe due to external_id uniqueness.

2) Correct transaction boundaries

Scenario: Create an order and reserve inventory in the same database.

BEGIN;
INSERT INTO orders (id, user_id, total_cents, status)
VALUES (gen_random_uuid(), $1, $2, 'created');

UPDATE inventory SET reserved = reserved + $3
WHERE sku = $4 AND (stock - reserved) >= $3;

-- Ensure that exactly one row was updated
IF NOT FOUND THEN ROLLBACK; RAISE 'Insufficient stock'; END IF;

COMMIT;

Same-DB updates belong in one ACID transaction. If inventory lived in another service, use a saga: mark order as pending, emit an event, and confirm when reservation succeeds.

3) Handling partial webhook failures

Goal: Deliver partner webhooks with at-least-once semantics and idempotency.

// Sender keeps retrying with backoff; includes signature and idempotency key
POST /webhook { event_id, type, data }
Headers: X-Idempotency-Key: event_id
// Receiver stores deliveries to dedupe
CREATE TABLE webhook_receipts (
  idempotency_key TEXT PRIMARY KEY,
  received_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

function handleWebhook(req) {
  const key = req.headers['X-Idempotency-Key'];
  db.transaction(() => {
    db.exec(
      "INSERT INTO webhook_receipts (idempotency_key) VALUES ($1) ON CONFLICT DO NOTHING",
      [key]
    );
    // Process business logic only if first time
    const inserted = db.rowCount == 1;
    if (inserted) process(req.body);
  });
  return 200; // 2xx signals success; otherwise sender retries
}

Tip: Use exponential backoff with jitter on the sender. Cap retries and route to a dead-letter log if repeatedly failing.

4) Eventual consistency with read models

Goal: Keep a denormalized read table fresh within 5 seconds for fast queries.

-- Events table fed by outbox or bus
CREATE TABLE events (
  id UUID PRIMARY KEY,
  type TEXT NOT NULL,
  payload JSONB NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Read model
CREATE TABLE order_summary (
  order_id UUID PRIMARY KEY,
  total_cents INT,
  item_count INT,
  last_event_at TIMESTAMPTZ
);

-- Projector
function project() {
  const evts = db.query("SELECT * FROM events WHERE id > $lastSeen ORDER BY created_at LIMIT 500");
  for (const e of evts) {
    switch (e.type) {
      case 'OrderCreated':
        db.exec("INSERT INTO order_summary(order_id, total_cents, item_count, last_event_at) VALUES ($1,$2,$3,$4) ON CONFLICT (order_id) DO UPDATE SET total_cents=$2, item_count=$3, last_event_at=$4", [e.payload.id, e.payload.total_cents, e.payload.item_count, e.created_at]);
        break;
      // handle other events
    }
  }
}

Communicate freshness in the API response, e.g., { data, maxLagSeconds: 3 } so clients understand consistency.

5) Safe schema evolution

Goal: Add new field customer_tier without breaking older clients.

-- Add with default and allow nulls initially
ALTER TABLE customers ADD COLUMN customer_tier TEXT DEFAULT 'standard';

-- Backfill gradually
UPDATE customers SET customer_tier = 'standard' WHERE customer_tier IS NULL;

-- API: treat field as optional; do not remove old fields until clients migrate

Rules: Prefer additive changes, keep old fields for a deprecation window, and version message formats if you must remove or change semantics.

Drills and quick exercises

  • Design an idempotent POST: choose a unique key, write the SQL upsert, and define what the server returns on conflict.
  • List three operations that must be in one DB transaction for your current feature.
  • Define retry policy for a webhook: max attempts, base delay, jitter, and cutoff time.
  • Write a reconciliation query to find orders missing corresponding invoices (or vice versa).
  • Sketch a read model and name the events required to build it.
  • Plan a schema change: describe rollout, backfill, compatibility window, and cleanup.

Common mistakes and debugging tips

  • Mistake: Relying on exactly-once delivery over the network. Tip: Aim for at-least-once with idempotent handlers and dedupe keys.
  • Mistake: Overusing distributed transactions. Tip: Prefer local ACID transactions and async messaging (outbox) across services.
  • Mistake: Missing unique constraints. Tip: Add database-level uniqueness for idempotency keys or natural keys.
  • Mistake: Infinite retries. Tip: Use bounded retries with backoff and dead-letter review.
  • Mistake: Breaking changes in payloads. Tip: Use additive evolution and explicit versioning.
  • Mistake: Invisible lag. Tip: Expose freshness or version timestamps in read APIs.
Debugging playbook
  • Duplicate records: Check for missing unique index; review retry logs for the same request ID.
  • Out-of-sync read model: Inspect projector offset vs latest event timestamp; look for poison events.
  • Webhook storm: Verify the receiver’s 2xx response; ensure idempotency keys are stable across retries.
  • Phantom missing links: Run left-join reconciliation queries to identify drift and backfill.

Mini project: Order-to-Fulfillment integration

Build a simple service that creates orders, emits events via outbox, updates a read model, and delivers webhooks to a mock partner with retries and idempotency.

Requirements
  • POST /orders is idempotent using a client-supplied key.
  • Order creation and outbox write are in one transaction.
  • A relay worker delivers outbox events to an /partner-hook endpoint with exponential backoff and jitter.
  • The partner endpoint is idempotent using the event_id as a key.
  • Read model order_summary is updated by a projector from events.
  • Provide a reconciliation job that detects orders without a corresponding order_summary row.
Suggested steps
  1. Define schema (orders, outbox, events, order_summary, webhook_receipts).
  2. Implement POST /orders with upsert and outbox write.
  3. Create the outbox relay with retry policy and dead-letter log.
  4. Implement the partner receiver with idempotency dedupe.
  5. Add the projector and expose GET /orders/{id}/summary with freshness metadata.
  6. Write a reconciliation script to report and fix drift.

Practical projects

  • Inventory reservation saga: pending order, reserve inventory, confirm or cancel via events.
  • User profile sync: upsert from an external CRM using CDC or periodic pulls with reconciliation.
  • Billing statement aggregator: build a denormalized monthly statement read model from payment events.

Subskills

  • Mapping To Databases — Model, upsert, and merge incoming data safely with constraints and indexes.
  • Transaction Boundaries — Decide what belongs in one transaction vs a saga or async flow.
  • Handling Partial Failures — Retries, backoff, dead letters, and compensating actions.
  • Eventual Consistency Patterns — Read models, projections, and clear SLAs for staleness.
  • Outbox Pattern Basics — Atomic business change + event, replayable and reliable.
  • Webhook Delivery Guarantees — At-least-once delivery, signatures, idempotent receivers.
  • Data Validation And Reconciliation — Detect and correct drift across systems.
  • Handling Schema Evolution — Backward-compatible changes and versioning strategies.

Next steps

  • Implement the mini project and run failure drills (drop network, duplicate requests, delayed events).
  • Add metrics: event lag, retry counts, dead-letter size, reconciliation diffs.
  • Then dive into API versioning and contract testing to protect integrations as they evolve.

Data Integration And Consistency — Skill Exam

This exam checks your understanding of idempotency, transactions, outbox, eventual consistency, webhooks, reconciliation, and schema evolution. You can take it for free. Anyone can view results; only logged-in users have their progress saved to their profile.Tips: Read carefully, some questions allow multiple answers. Aim for 70% to pass.

11 questions70% to pass

Have questions about Data Integration And Consistency?

AI Assistant

Ask questions about this tool