Chapter 01 · Why NoSQL Exists: Workloads, Scale, Flexibility, and Polyglot Persistence

Polyglot Persistence: Assigning Systems by Invariant, Query Shape, Latency, and Ownership

Design polyglot persistence around explicit ownership, demonstrate a concrete dual-write failure, and repair it with an atomic outbox, idempotent projection, replay, rebuild, and reconciliation reasoning.

Beginner100–120 minutesDual-write failure + outbox/replay labPython stdlib · temporary SQLite/JSON stateLast reviewed: August 2026

Learning outcomes

Polyglot persistence means using more than one data technology because different data paths have different invariants and access patterns. It does not mean every service gets its favorite database. Every added store creates a second question: how does it stay correct enough when the source changes or failures interrupt synchronization?

01

Define system of record, derived read model, cache, search index, and ownership boundary precisely.

02

Explain why writing a source database and a second store in two independent operations creates a dual-write failure window.

03

Use an outbox record committed with source state to make change publication replayable without claiming magical exactly-once delivery.

04

Design a derived view that is idempotent, rebuildable, observable, and never mistaken for an authorization source.

05

Account for the operational cost of every additional database before approving polyglot architecture.

Lab safety and scope

The lab creates a temporary SQLite database and JSON file under the operating system’s temporary directory. It intentionally simulates a process crash by skipping one write; it does not kill processes, change firewall rules, alter the host clock, or require Docker.

1. Ownership first: source of truth versus derived purpose

A system of record is the authoritative owner for a fact or invariant. A derived view is a representation computed from source data to serve a different access pattern. A cache is usually reconstructable temporary state. A search index is usually a derived retrieval view. A graph projection may derive edges from transactional records. These labels are responsibilities, not product types.

For AtlasMart, payment and order acceptance remain authoritative in one transactional path. Search can receive an eventually updated order or product projection. If the search index disappears, AtlasMart should be able to rebuild it from authoritative data and retained change history. If the order database disappears, rebuilding from search is not an acceptable recovery plan.

Component Authority Freshness expectation Recovery expectation
Order/payment store Authoritative Synchronous for checkout invariants Backups + transaction/change history + tested restore
Search index Derived Bounded lag, for example tens of seconds Rebuild/replay from source/change log
Session cache Ephemeral/derived Very fresh while present Expire/recreate; do not make unique durable facts live only here
Fraud graph Derived analytical/serving view Defined per detection use case Replay/backfill plus reconciliation

2. The dual-write trap

The naive workflow is: commit order → update search index → return success. If the process crashes after the database commit but before the index update, the order exists but the derived view does not. Reversing the order is not safer: the index can expose data whose source transaction later fails. Retries can create duplicate side effects unless each operation is idempotent.

A distributed transaction protocol can coordinate some resources, but that introduces availability, blocking, topology, and product-support constraints and is not a universal answer. A common alternative is a transactional outbox: commit the business state and an event/outbox row in the same local transaction. A separate projector publishes or applies those events and can retry after failure.

Important non-guarantee

An outbox does not create end-to-end exactly-once delivery. The projector can crash after applying a change but before marking an event processed. Correct consumers therefore use idempotent operations, deduplication keys, or version checks and support replay.

3. Run the broken path and repaired path

python · dual-write failure, transactional outbox, idempotent replay, rebuild
import json, sqlite3, tempfilefrom pathlib import Pathroot=Path(tempfile.mkdtemp(prefix='atlasmart-polyglot-'))db=root/'atlasmart.db'; view=root/'search_view.json'print('AtlasMart Lesson 4 — system of record + replayable derived view')print('workspace:', root)con=sqlite3.connect(db)con.execute('PRAGMA journal_mode=WAL')con.executescript('''CREATE TABLE orders(id TEXT PRIMARY KEY, customer_id TEXT NOT NULL, status TEXT NOT NULL, total_cents INTEGER NOT NULL);CREATE TABLE outbox(event_id TEXT PRIMARY KEY, event_type TEXT NOT NULL, payload TEXT NOT NULL, processed INTEGER NOT NULL DEFAULT 0);''')def load_view():    return json.loads(view.read_text()) if view.exists() else {}def save_view(v): view.write_text(json.dumps(v, indent=2, sort_keys=True))# WRONG: dual write. Database commit succeeds, process 'crashes' before derived view write.with con:    con.execute('INSERT INTO orders VALUES (?,?,?,?)',('o-bad','c-1','paid',4200))print('\nWrong dual-write state after simulated crash:')print('system of record has o-bad =', bool(con.execute("SELECT 1 FROM orders WHERE id='o-bad'").fetchone()))print('derived view has o-bad =', 'o-bad' in load_view())# REPAIR: order + outbox event commit atomically.payload={'order_id':'o-good','customer_id':'c-2','status':'paid','total_cents':9900}with con:    con.execute('INSERT INTO orders VALUES (?,?,?,?)', tuple(payload.values()))    con.execute('INSERT INTO outbox(event_id,event_type,payload) VALUES (?,?,?)',('evt-1001','OrderPaid',json.dumps(payload)))print('\nAtomic source transaction committed order + outbox event.')# Idempotent projector: replaying same event overwrites same key instead of duplicating side effects.def project_pending():    v=load_view()    rows=con.execute('SELECT event_id,payload FROM outbox WHERE processed=0 ORDER BY event_id').fetchall()    for event_id,payload_json in rows:        p=json.loads(payload_json)        v[p['order_id']]={'customer_id':p['customer_id'],'status':p['status'],'total_cents':p['total_cents'],'source_event':event_id}        save_view(v)        with con: con.execute('UPDATE outbox SET processed=1 WHERE event_id=?',(event_id,))    return len(rows)print('projected events =', project_pending())print('projected events on replay =', project_pending())print('derived view:', json.dumps(load_view(), indent=2, sort_keys=True))# Rebuild proves derived data is disposable when source + event history are retained.view.unlink()with con: con.execute('UPDATE outbox SET processed=0')print('\nRebuild after deleting derived view: projected events =', project_pending())print('recovered o-good =', load_view().get('o-good'))print('Note: o-bad cannot be reconstructed from the outbox because the wrong path never emitted an event. Reconciliation would be required.')

Verified generation-time output

text · temporary local lab output; temporary path changes each run
AtlasMart Lesson 4 — system of record + replayable derived viewworkspace: /tmp/atlasmart-polyglot-xykkxckuWrong dual-write state after simulated crash:system of record has o-bad = Truederived view has o-bad = FalseAtomic source transaction committed order + outbox event.projected events = 1projected events on replay = 0derived view: {  "o-good": {    "customer_id": "c-2",    "source_event": "evt-1001",    "status": "paid",    "total_cents": 9900  }}Rebuild after deleting derived view: projected events = 1recovered o-good = {'customer_id': 'c-2', 'source_event': 'evt-1001', 'status': 'paid', 'total_cents': 9900}Note: o-bad cannot be reconstructed from the outbox because the wrong path never emitted an event. Reconciliation would be required.

The first state is intentionally inconsistent: o-bad exists in the source but not the derived view. That is the exact failure window a two-step dual write creates. The repaired path commits o-good and evt-1001 together. The projector can run later, and running it again produces zero new work because processed events are tracked.

Then the lab deletes the derived JSON file and resets the outbox processed flags. Replaying reconstructs o-good. It cannot reconstruct o-bad because the broken path never recorded an event. This is why production designs also need reconciliation: compare source state with derived state and repair historical gaps that predate the reliable publication mechanism.

4. Every additional store adds a failure matrix

Adding search, cache, graph, or analytical stores expands the architecture. Now an incident can affect the source, the change capture mechanism, the queue or outbox reader, the projector, the derived store, or the network between them. Monitoring only “database is up” is insufficient.

Signal What it tells you Failure it can reveal
Oldest unprocessed outbox age Publication lag Projector stopped or downstream unavailable
Projection apply error rate Transformation/schema compatibility Poison event or incompatible deployment
Source vs derived sampled checksum/count Convergence Dropped events or projector bug
Replay throughput and remaining backlog Recovery capacity Whether the system can catch up before freshness SLO is violated
Derived-store authorization tests Security boundary Accidental exposure of fields/tenants in a convenience index

5. Deliberately wrong approach: let the derived store become authoritative

A team may discover that search is easier to query than the transactional source and begin reading authorization, availability, or payment state from it. That silently changes the architecture: a view with allowed lag is now making correctness decisions. During projector delay, a revoked tenant permission could remain visible or an out-of-stock product could remain purchasable.

The repair is explicit read-model scope. Search may answer “which products match these words?” Checkout must revalidate current price, inventory, tenant authorization, and payment rules against the authoritative owners before committing a purchase. A derived view can accelerate discovery without owning the invariant.

6. Production judgment: polyglot architecture is a budgeted dependency

Add a store only when the requirement justifies its synchronization and operations tax. Record the owner team, source of record, data classification, publication mechanism, ordering scope, idempotency key, allowed lag, retention, replay capacity, rebuild steps, backup policy, access control, metrics, incident runbook, cost, and decommission plan. If those fields are unknown, the architecture is not ready.

The next lesson turns this chapter’s reasoning into a reusable decision notebook. Instead of choosing MongoDB, Redis, Cassandra, Neo4j, or a search engine by reputation, AtlasMart will first write down volumes, key distributions, latency percentiles, invariants, availability, recovery, compliance, skills, and cost constraints.

Verification checklist

  • The wrong path visibly leaves source and derived state inconsistent.
  • The source business row and outbox event share one local transaction.
  • The projector is replayable and its update is idempotent for the same order key/event.
  • Deleting the derived view demonstrates rebuildability.
  • The lesson explicitly states that outbox/replay does not guarantee magical exactly-once delivery.

Check your understanding

  1. What makes a store a system of record rather than simply “the main database”?
  2. Why can neither ordering of two independent writes eliminate the dual-write failure window?
  3. What property does the outbox transaction provide?
  4. Why must a projector be idempotent even when events have unique IDs?
  5. Why should checkout revalidate data that was discovered through a search index?
Review the answers

It owns the authoritative fact/invariant and has the recovery/security responsibilities for that fact. Product type or query popularity does not define authority.

If source is first, a crash can omit the derived write; if derived is first, the source transaction can fail after the derived state appears. Without shared atomic coordination, a failure can split the outcome.

It makes the source mutation and intent-to-publish one atomic local commit, so a later worker can discover/retry publication after process failure.

The worker may apply an event and crash before recording completion, causing the same event to be delivered/applied again. The effect must therefore tolerate replay.

The index is a derived view with bounded freshness and should not silently own inventory, payment, or authorization invariants. Checkout must confirm current authoritative state.

Authoritative references

  • SQLite atomic commit — Official description of SQLite’s transactional durability/atomic-commit mechanisms used as the local source-of-record lab.
  • SQLite write-ahead logging — Official WAL documentation; the lab enables WAL for the temporary database while remaining single-process.
  • Debezium outbox event router — Official implementation documentation for the transactional outbox pattern in CDC-based architectures.
  • Polyglot Persistence — Widely cited architecture framing for choosing different persistence technologies by data/workload need; use as design context rather than protocol specification.
  • Dynamo: Amazon’s Highly Available Key-value Store — Primary example of explicit application-visible tradeoffs and ownership semantics in a distributed store.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.