Loading learning content...
We've explored each ACID property in depth—Atomicity's all-or-nothing guarantee, Consistency's invariant preservation, Isolation's concurrency protection, and Durability's permanence promise. Now it's time to bring them together.
In production systems, ACID properties don't operate in isolation. They form an integrated system of guarantees that work together to ensure database reliability. Understanding how they interplay—and when you might intentionally relax them—is the mark of a mature systems engineer.
This page synthesizes everything into practical guidance: how ACID manifests in real systems, when strong ACID matters most, when relaxations are acceptable, and how to make informed trade-offs in system design.
This page covers: the complete ACID transaction lifecycle, how the four properties reinforce each other, real-world scenarios requiring strong ACID, scenarios where ACID relaxation is acceptable, the performance costs of full ACID compliance, practical patterns for working with transactions, and a decision framework for choosing appropriate guarantees.
Let's trace a complete transaction through a database, showing how each ACID property contributes at different phases.
Scenario: Bank Transfer
Alice transfers $500 to Bob. This involves debiting Alice's account and crediting Bob's account—a classic two-operation transaction.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
┌────────────────────────────────────────────────────────────────────────┐│ PHASE 1: BEGIN TRANSACTION │├────────────────────────────────────────────────────────────────────────┤│ • Transaction assigned a unique ID (e.g., Tx-12345) ││ • ISOLATION: Snapshot taken (for SI/SSI); locks may be acquired ││ • ATOMICITY: Transaction registered; WAL will track its operations │└────────────────────────────────────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────────┐│ PHASE 2: EXECUTE OPERATIONS │├────────────────────────────────────────────────────────────────────────┤│ Operation 1: UPDATE accounts SET balance = balance - 500 ││ WHERE user_id = 'alice' ││ ││ • ATOMICITY: WAL record written (before-image: 1000, after: 500) ││ • ISOLATION: Row locked or version created depending on implementation ││ • CONSISTENCY: CHECK constraint verified (balance >= 0) ││ ││ Operation 2: UPDATE accounts SET balance = balance + 500 ││ WHERE user_id = 'bob' ││ ││ • ATOMICITY: WAL record written (before-image: 300, after: 800) ││ • ISOLATION: Row locked or version created ││ • CONSISTENCY: Constraints verified │└────────────────────────────────────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────────┐│ PHASE 3: PREPARE TO COMMIT │├────────────────────────────────────────────────────────────────────────┤│ • CONSISTENCY: All deferred constraints checked ││ • CONSISTENCY: Triggers executed ││ • ISOLATION (SSI): Check for serialization conflicts ││ ││ If any check fails → ABORT (proceed to Phase 4a) ││ If all checks pass → COMMIT (proceed to Phase 4b) │└────────────────────────────────────────────────────────────────────────┘ │ │ ▼ ▼┌──────────────────────────────┐ ┌──────────────────────────────────────┐│ PHASE 4a: ABORT │ │ PHASE 4b: COMMIT │├──────────────────────────────┤ ├──────────────────────────────────────┤│ • ATOMICITY: Roll back using │ │ • ATOMICITY: Write COMMIT to WAL ││ WAL before-images │ │ • DURABILITY: fsync WAL to disk ││ • ISOLATION: Release locks │ │ • DURABILITY: (optional) wait for ││ Drop uncommitted versions │ │ replica acknowledgment ││ • Result: As if tx never │ │ • ISOLATION: Release locks / make ││ happened │ │ versions visible ││ │ │ • Result: Changes permanent │└──────────────────────────────┘ └──────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ CLIENT RECEIVES "COMMIT OK" │ │ │ │ The transaction is now: │ │ • ATOMIC: Both updates applied or neither│ │ • CONSISTENT: All invariants preserved │ │ • ISOLATED: No interference from others │ │ • DURABLE: Survives any subsequent crash │ └──────────────────────────────────────────┘ACID properties are deeply interconnected. Each property contributes to the overall reliability, and weakening one often impacts others.
The Dependency Web:
12345678910111213141516171819202122232425262728
┌─────────────────┐ │ CONSISTENCY │ │ (Valid States) │ └────────▲────────┘ │ supported by │ supported by ┌──────────────────────┤────────────────────────┐ │ │ │ │ │ │ ┌─────┴──────┐ ┌─────┴──────┐ ┌──────┴──────┐ │ ATOMICITY │ │ ISOLATION │ │ DURABILITY │ │ (All/None) │◄───────►│ (Separate) │◄────────►│ (Permanent) │ └────────────┘ └────────────┘ └─────────────┘ │ │ │ │ shares │ both │ └───────►│◄────────────┘ require WAL │ │ │ │ ┌─────┴─────┐ └────────────┘ │ WAL │ │(Log Files)│ └───────────┘ Key Relationships:─────────────────• ATOMICITY + DURABILITY: Both rely on WAL for recovery• ATOMICITY + ISOLATION: Both use transaction boundaries• ISOLATION + CONSISTENCY: Isolation prevents concurrent violations• DURABILITY + CONSISTENCY: Preserved consistent state survives| If This Property Fails... | These Properties Are Compromised... |
|---|---|
| Atomicity | Consistency (partial updates violate invariants) |
| Consistency | Data validity (incorrect data gets persisted) |
| Isolation | Atomicity & Consistency (concurrent interference) |
| Durability | All properties become meaningless (data lost) |
Don't think of ACID as four independent boxes to check. They form an integrated system where each component supports the others. You can't fully evaluate one property without considering its interactions with the others.
Certain domains absolutely require full ACID guarantees. In these contexts, any compromise risks serious consequences—financial loss, legal liability, or safety hazards.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// E-commerce checkout: Must be fully ACIDasync function processCheckout(cartId: string, paymentDetails: PaymentDetails) { return await prisma.$transaction(async (tx) => { // 1. Lock and validate cart const cart = await tx.cart.findUnique({ where: { id: cartId }, include: { items: { include: { product: true } } } }); if (!cart || cart.status !== 'active') { throw new Error('Invalid cart'); } // 2. Check inventory atomically (prevent overselling) for (const item of cart.items) { const updated = await tx.product.updateMany({ where: { id: item.productId, inventory: { gte: item.quantity } // Only if enough stock }, data: { inventory: { decrement: item.quantity } } }); if (updated.count === 0) { // ATOMICITY: Entire transaction will roll back throw new Error(`Insufficient inventory for ${item.product.name}`); } } // 3. Process payment const paymentResult = await processPayment(paymentDetails, cart.total); if (!paymentResult.success) { // ATOMICITY: Inventory decrements will be rolled back throw new Error('Payment failed'); } // 4. Create order record const order = await tx.order.create({ data: { userId: cart.userId, total: cart.total, paymentId: paymentResult.id, status: 'confirmed', items: { create: cart.items.map(item => ({ productId: item.productId, quantity: item.quantity, price: item.price })) } } }); // 5. Mark cart as completed await tx.cart.update({ where: { id: cartId }, data: { status: 'completed', orderId: order.id } }); return order; // If ANY step fails, ALL changes roll back // CONSISTENCY: Inventory matches orders // ISOLATION: No race conditions with concurrent checkouts // DURABILITY: Order survives any crash after commit }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable, timeout: 30000, maxWait: 10000 });}Not every operation needs full ACID. For some use cases, relaxing guarantees enables better performance, availability, or scalability. The key is understanding the trade-offs.
Scenarios Where Relaxation May Be Acceptable:
| Use Case | Acceptable Relaxation | Rationale |
|---|---|---|
| View counts, like counts | Eventual consistency, async writes | Approximate counts are fine; exact count not critical |
| User activity logging | Async writes, no immediate consistency | Logs can be eventually consistent; some loss acceptable |
| Cache warming | No durability needed | Cache is rebuilt anyway; not source of truth |
| Session data | May accept some loss | Users can re-authenticate; convenience, not critical |
| Analytics/metrics | Approximate is acceptable | Statistical accuracy more important than precision |
| Draft saves (auto-save) | Eventual durability, async | Explicit save is the commit point; drafts can be lost |
| Real-time feeds | Slight staleness OK | Freshness less important than availability |
123456789101112131415161718192021222324252627282930313233343536
// View count: eventual consistency is fineasync function incrementViewCount(articleId: string) { // Fire-and-forget: we don't wait for confirmation // Some increments may be lost on crash—that's acceptable prisma.article.update({ where: { id: articleId }, data: { viewCount: { increment: 1 } } }).catch(err => { // Log but don't throw—view count loss is acceptable console.warn(`Failed to increment view count: ${err.message}`); }); // Return immediately—don't block user experience // for a non-critical operation} // Or even better: batch updatesconst viewBuffer = new Map<string, number>(); function bufferViewIncrement(articleId: string) { viewBuffer.set(articleId, (viewBuffer.get(articleId) || 0) + 1);} // Flush periodicallysetInterval(async () => { const updates = [...viewBuffer.entries()]; viewBuffer.clear(); // Bulk update—much more efficient for (const [articleId, count] of updates) { await prisma.article.update({ where: { id: articleId }, data: { viewCount: { increment: count } } }).catch(console.warn); }}, 30000); // Every 30 secondsACID guarantees come with performance overhead. Understanding these costs helps you make informed decisions about when full ACID is necessary and when relaxation might be beneficial.
Atomicity Costs:
Consistency Costs:
Isolation Costs:
Durability Costs:
| Configuration | Writes/Second | Latency (p95) | Data Safety |
|---|---|---|---|
| fsync=off, no replication | 50,000+ | < 1ms | Low (data loss on crash) |
| fsync=on, no replication | 5,000-10,000 | 5-10ms | Medium (survives crash) |
| fsync=on, async replication | 4,000-8,000 | 8-15ms | Medium (may lose recent data) |
| fsync=on, sync replication | 1,000-3,000 | 20-50ms | High (survives server failure) |
| Serializable + sync repl | 500-2,000 | 30-100ms | Maximum |
If ACID performance is a bottleneck: (1) Use group commit for better batching, (2) Use async replication for reads, sync for writes, (3) Partition workloads—critical data gets full ACID, analytics get relaxed, (4) Use connection pooling to reduce connection overhead, (5) Consider read replicas to offload read traffic.
These patterns help you use ACID effectively while avoiding common pitfalls.
Pattern 1: Keep Transactions Short
Long transactions hold locks, block other transactions, and increase the chance of conflicts. Keep transaction scope minimal.
12345678910111213141516171819202122232425262728293031323334353637
// ❌ BAD: Long transaction with external callasync function processOrderBad(orderId: string) { await prisma.$transaction(async (tx) => { const order = await tx.order.findUnique({ where: { id: orderId } }); // This HTTP call might take seconds—holding locks the whole time! const shippingLabel = await externalShippingService.createLabel(order); await tx.order.update({ where: { id: orderId }, data: { shippingLabel, status: 'shipped' } }); });} // ✅ GOOD: Minimize transaction scopeasync function processOrderGood(orderId: string) { // Read data outside transaction const order = await prisma.order.findUnique({ where: { id: orderId } }); // External call outside transaction const shippingLabel = await externalShippingService.createLabel(order); // Short transaction for the actual update await prisma.$transaction(async (tx) => { // Verify order hasn't changed const current = await tx.order.findUnique({ where: { id: orderId } }); if (current.status !== 'pending') { throw new Error('Order status changed'); } await tx.order.update({ where: { id: orderId }, data: { shippingLabel, status: 'shipped' } }); });}Pattern 2: Optimistic Concurrency Control
Instead of locking pessimistically, detect conflicts at update time using version numbers.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
// Table has a 'version' column that increments on each update async function updateWithOptimisticLocking( userId: string, newEmail: string) { // Read current version const user = await prisma.user.findUnique({ where: { id: userId } }); // Apply business logic (can take time, no locks held) const validatedEmail = await validateEmail(newEmail); // Update only if version hasn't changed const result = await prisma.user.updateMany({ where: { id: userId, version: user.version // Only update if version matches }, data: { email: validatedEmail, version: { increment: 1 } // Bump version } }); if (result.count === 0) { // Someone else modified the row—retry or fail throw new Error('Concurrent modification detected'); } return result;} // Retry wrapper for optimistic lockingasync function withRetry<T>( operation: () => Promise<T>, maxRetries = 3): Promise<T> { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { if (error.message === 'Concurrent modification detected' && attempt < maxRetries) { await new Promise(r => setTimeout(r, 100 * attempt)); // Backoff continue; } throw error; } } throw new Error('Max retries exceeded');}Pattern 3: Idempotent Operations
Design operations so they can be safely retried without side effects.
12345678910111213141516171819202122232425262728293031323334353637383940
// ❌ NOT IDEMPOTENT: Multiple calls create multiple recordsasync function createPaymentBad(orderId: string, amount: number) { return prisma.payment.create({ data: { orderId, amount, status: 'pending' } });} // ✅ IDEMPOTENT: Use idempotency keyasync function createPaymentGood( orderId: string, amount: number, idempotencyKey: string) { // First, check if we've already processed this request const existing = await prisma.payment.findUnique({ where: { idempotencyKey } }); if (existing) { return existing; // Return the same result as before } // Create with the idempotency key (unique constraint) try { return await prisma.payment.create({ data: { orderId, amount, status: 'pending', idempotencyKey // Unique constraint prevents duplicates } }); } catch (error) { if (error.code === 'P2002') { // Unique constraint violation // Race condition—another request created it first return prisma.payment.findUnique({ where: { idempotencyKey } }); } throw error; }}ACID isn't exclusive to traditional relational databases. Many modern systems provide ACID-like guarantees, though sometimes with different trade-offs.
NewSQL Databases:
Databases like CockroachDB, Google Spanner, and TiDB provide full ACID across distributed clusters. They use sophisticated protocols (Percolator, TrueTime) to achieve serializable isolation at global scale.
| Database Type | Examples | ACID Support | Notes |
|---|---|---|---|
| Traditional RDBMS | PostgreSQL, MySQL, Oracle | Full ✓ | Gold standard; well-understood semantics |
| NewSQL | CockroachDB, Spanner, TiDB | Full ✓ | Distributed ACID; higher latency |
| Document Stores | MongoDB 4.0+ | Single-document + Multi-doc ✓ | Added multi-doc transactions in 4.0 |
| Key-Value | FoundationDB | Full ✓ | ACID-compliant distributed KV |
| Key-Value | Redis | Limited | Single-command atomic; MULTI/EXEC not durable |
| Wide-Column | Cassandra, HBase | Limited | Row-level atomicity only |
| Time-Series | TimescaleDB | Full ✓ (PostgreSQL) | Inherits ACID from PostgreSQL |
When a NoSQL database claims 'ACID support,' read the documentation carefully. Many provide atomicity only for single-document operations, not multi-document transactions. Some offer 'eventual durability.' Understand exactly what guarantees you're getting.
Use this framework when designing systems to determine the appropriate level of ACID guarantees.
123456789101112131415161718192021222324252627282930313233343536373839404142434445
┌────────────────────────────────────────────────────────────────────────┐│ ACID REQUIREMENT ASSESSMENT │└────────────────────────────────────────────────────────────────────────┘ QUESTION 1: Is money, legal standing, or safety involved?───────────────────────────────────────────────────────── YES → FULL ACID REQUIRED. No shortcuts. NO → Continue assessment. QUESTION 2: Can this data be regenerated or refetched?───────────────────────────────────────────────────────── YES → Durability relaxation may be acceptable. Consider: async writes, write-behind caching. NO → Durability is required. Ensure fsync and replication. QUESTION 3: Is temporary inconsistency visible to users?───────────────────────────────────────────────────────── YES, and it matters → Full consistency required. YES, but acceptable → Eventual consistency may work. NO → Eventual consistency is likely fine. QUESTION 4: Do multiple operations need to succeed together?───────────────────────────────────────────────────────── YES → Atomicity required. Use transactions. NO → Single operations can be auto-committed. QUESTION 5: Do concurrent users modify the same data?───────────────────────────────────────────────────────── YES, frequently → Higher isolation level needed. Consider: Serializable, explicit locking. YES, rarely → Optimistic locking may suffice. NO → Read Committed is usually fine. QUESTION 6: What is the performance requirement?───────────────────────────────────────────────────────── Latency-critical (< 10ms) → May need async replication, connection pooling. Consider: eventual durability for non-critical data. Throughput-critical (> 10k TPS) → Need group commit, connection pooling, possibly sharding. Consider: batching writes, async processing. Moderate requirements → Standard ACID is fine. Don't over-optimize.If you're unsure whether you need full ACID, start with full ACID. It's much easier to relax guarantees later (when you understand your workload better) than to add them (which may require architectural changes). Premature optimization toward weaker guarantees has caused many production incidents.
You've now completed a comprehensive study of ACID properties—the foundational guarantees that make databases reliable repositories for critical data.
You now have a deep understanding of ACID properties—what they guarantee, how they're implemented, when they're essential, and when relaxation is acceptable. This knowledge is foundational for database selection, transaction design, and system architecture. You can now reason precisely about data guarantees in any system design discussion.