Cache-Aside vs Write-Through vs Write-Behind

By Oleksandr Andrushchenko — Published on — Modified on
0 Likes
0 Dislikes
Cache-Aside vs Write-Through vs Write-Behind
Cache-Aside vs Write-Through vs Write-Behind

Cache-aside, write-through, and write-behind define how applications coordinate cached data with authoritative storage. Each pattern changes read latency, write latency, consistency, durability, and failure recovery.

The correct strategy depends on whether the database must be updated synchronously, whether stale reads are acceptable, whether acknowledged writes can wait for persistence, and how the system recovers when the cache, database, network, or background worker fails.

Table of Contents

Why Cache Write Strategies Matter

A cache is another copy of application data. Once data changes, the system must coordinate at least two storage locations with different durability, latency, and failure characteristics.

Every cached write raises several production questions:

  • Which system is authoritative?
  • Should the database or cache be updated first?
  • Can the client receive success before the database is updated?
  • What happens when one write succeeds and the other fails?
  • How are duplicate requests and retries handled?
  • How are stale or out-of-order updates prevented?
Application write
       |
       +---- Database
       |
       +---- Cache

Possible outcomes:

Database succeeds + cache succeeds
Database succeeds + cache fails
Database fails    + cache succeeds
Client times out after one or both operations

Cache-aside, write-through, and write-behind place this coordination in different parts of the request path.

Strategy Synchronous Write Target Database Timing Typical Authority
Cache-aside Database Immediate Database
Write-through Database and cache Immediate Database after coordinated write
Write-behind Cache or durable buffer Delayed Temporary latest state may exist outside the database

Cache-Aside

Cache-aside, also called lazy loading, keeps the database authoritative and lets the application manage cached values. Data enters the cache only after a read miss, and writes usually invalidate cached entries after the database transaction commits.

Read Flow

Read request
    |
    v
Check cache
    |
    +---- hit ----> Return cached value
    |
    +---- miss
           |
           v
       Read database
           |
           v
       Store in cache
           |
           v
       Return value

Only requested records consume cache memory. This works well when the database contains a large dataset but the active working set is relatively small.

The first request after expiration is slower because it performs both a failed cache lookup and a database query. Concurrent requests for the same missing key can also overload the database unless the application coalesces them.

Write Flow

The most common write sequence updates the database first and deletes the cached value after commit.

Update request
    |
    v
Database transaction
    |
    v
Commit succeeds
    |
    v
Delete cache key
    |
    v
Next read reloads committed state

Deleting is usually safer than directly replacing the cache entry. The next read rebuilds the representation from committed database state, including generated values, joins, defaults, and calculated fields.

A cache-aside race can still produce stale data:

Request A misses cache and reads database value v1
Request B updates database to v2
Request B deletes cache key
Request A stores old value v1 in cache

Finite TTLs, version checks, request coalescing, and event-driven invalidation reduce this risk. Strictly consistent workflows should bypass the cache and read authoritative storage.

Python Example

from __future__ import annotations

import asyncio
import json
import logging
import random
from dataclasses import asdict, dataclass
from typing import Protocol

from redis.asyncio import Redis
from redis.exceptions import RedisError


logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class Shipment:
    shipment_id: str
    account_id: int
    status: str
    carrier_code: str | None
    tracking_number: str | None
    version: int


class ShipmentRepository(Protocol):
    async def get(
        self,
        account_id: int,
        shipment_id: str,
    ) -> Shipment | None:
        ...


class ShipmentCacheAsideService:
    def __init__(
        self,
        redis: Redis,
        repository: ShipmentRepository,
        active_ttl_seconds: int = 60,
        completed_ttl_seconds: int = 3_600,
    ) -> None:
        self._redis = redis
        self._repository = repository
        self._active_ttl_seconds = active_ttl_seconds
        self._completed_ttl_seconds = completed_ttl_seconds
        self._locks: dict[str, asyncio.Lock] = {}

    @staticmethod
    def _key(account_id: int, shipment_id: str) -> str:
        return (
            f"shipment:v1:"
            f"account:{account_id}:"
            f"id:{shipment_id}"
        )

    @staticmethod
    def _with_jitter(ttl_seconds: int) -> int:
        jitter = max(1, int(ttl_seconds * 0.15))
        return ttl_seconds + random.randint(-jitter, jitter)

    async def get(
        self,
        account_id: int,
        shipment_id: str,
    ) -> Shipment | None:
        key = self._key(account_id, shipment_id)

        cached = await self._read_cache(key)
        if cached is not None:
            return cached

        lock = self._locks.setdefault(key, asyncio.Lock())

        async with lock:
            # Another request may have populated the key while this
            # request was waiting for the local per-key lock.
            cached = await self._read_cache(key)
            if cached is not None:
                return cached

            shipment = await self._repository.get(
                account_id=account_id,
                shipment_id=shipment_id,
            )

            if shipment is None:
                return None

            ttl_seconds = (
                self._completed_ttl_seconds
                if shipment.status in {"delivered", "cancelled"}
                else self._active_ttl_seconds
            )

            try:
                await self._redis.set(
                    key,
                    json.dumps(asdict(shipment)),
                    ex=self._with_jitter(ttl_seconds),
                )
            except RedisError:
                # Cache population is optional. The authoritative
                # database result can still be returned.
                logger.exception(
                    "Cache population failed",
                    extra={"cache_key": key},
                )

            return shipment

    async def invalidate(
        self,
        account_id: int,
        shipment_id: str,
    ) -> None:
        key = self._key(account_id, shipment_id)

        try:
            await self._redis.delete(key)
        except RedisError:
            # Invalidation workers should retry this failure.
            logger.exception(
                "Cache invalidation failed",
                extra={"cache_key": key},
            )
            raise

    async def _read_cache(
        self,
        key: str,
    ) -> Shipment | None:
        try:
            cached = await self._redis.get(key)
        except RedisError:
            logger.exception(
                "Cache lookup failed",
                extra={"cache_key": key},
            )
            return None

        if cached is None:
            return None

        return Shipment(**json.loads(cached))

The cache fails open for reads because PostgreSQL remains authoritative. Invalidation raises an error so a durable event consumer can retry it. TTL jitter reduces synchronized expirations.

The database update and invalidation event should commit in one transaction:

BEGIN;

UPDATE shipments
SET status = :new_status,
    version = version + 1,
    updated_at = CURRENT_TIMESTAMP
WHERE account_id = :account_id
  AND shipment_id = :shipment_id
  AND version = :expected_version
RETURNING version;

INSERT INTO outbox_events (
    event_id,
    aggregate_type,
    aggregate_id,
    aggregate_version,
    event_type,
    payload
)
VALUES (
    :event_id,
    'shipment',
    :shipment_id,
    :new_version,
    'shipment.updated',
    jsonb_build_object(
        'account_id', :account_id,
        'shipment_id', :shipment_id,
        'version', :new_version
    )
);

COMMIT;

An outbox consumer deletes the cache key only after the database transaction is durable.

Advantages, Disadvantages, and Use Cases

Advantages:

  • The database remains the clear source of truth.
  • Only frequently accessed records consume cache memory.
  • Cache failures do not need to block authoritative writes.
  • The application remains correct when the cache is empty.
  • The pattern can be introduced gradually.

Disadvantages:

  • The first request after a miss or expiration is slower.
  • Concurrent misses can overload the database.
  • Invalidation races can temporarily restore stale data.
  • Cache-management logic is distributed through application code.
  • A full cache outage can redirect large read volume to the database.

When to use:

  • product and shipment details
  • user profiles
  • tenant configuration
  • read-heavy APIs
  • data with bounded staleness tolerance
  • systems where the database must remain authoritative

When not to use: data that must always exist in the cache immediately after a write, or workloads where cache misses cannot safely reach the database.

Write-Through

Write-through updates the database and cache within the synchronous request path. The client receives success only after the durable write completes, and the cache receives the committed representation immediately.

The pattern is usually implemented through a dedicated data-access service. Allowing unrelated application code to write the database and cache independently increases partial-failure risk.

Read and Write Flow

Update request
    |
    v
Write-through service
    |
    +---- write database
    |          |
    |          v
    |       commit
    |
    +---- update cache
    |
    v
Return success

Database-first write-through is safer because the cache never exposes state that failed database validation.

If the database commits but the cache update fails, the request should not usually be reported as a failed business write. Otherwise, the client may retry an operation that already committed. The service can delete the key, log the cache failure, and allow the next read to repopulate it.

Cache-first write-through is dangerous unless a single system coordinates both writes transactionally. A database failure after a successful cache update can expose data that was never committed.

Python Example

from __future__ import annotations

import json
import logging
from dataclasses import asdict, dataclass

from redis.asyncio import Redis
from redis.exceptions import RedisError
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine


logger = logging.getLogger(__name__)


class ConcurrentPreferenceUpdateError(RuntimeError):
    pass


@dataclass(frozen=True)
class UserPreference:
    account_id: int
    user_id: int
    timezone: str
    page_size: int
    version: int


class PreferenceWriteThroughService:
    def __init__(
        self,
        engine: AsyncEngine,
        redis: Redis,
        ttl_seconds: int = 3_600,
    ) -> None:
        self._engine = engine
        self._redis = redis
        self._ttl_seconds = ttl_seconds

    @staticmethod
    def _key(account_id: int, user_id: int) -> str:
        return (
            f"preference:v1:"
            f"account:{account_id}:"
            f"user:{user_id}"
        )

    async def update(
        self,
        account_id: int,
        user_id: int,
        timezone: str,
        page_size: int,
        expected_version: int,
    ) -> UserPreference:
        async with self._engine.begin() as connection:
            result = await connection.execute(
                text(
                    """
                    UPDATE user_preferences
                    SET timezone = :timezone,
                        page_size = :page_size,
                        version = version + 1,
                        updated_at = CURRENT_TIMESTAMP
                    WHERE account_id = :account_id
                      AND user_id = :user_id
                      AND version = :expected_version
                    RETURNING
                        account_id,
                        user_id,
                        timezone,
                        page_size,
                        version
                    """
                ),
                {
                    "account_id": account_id,
                    "user_id": user_id,
                    "timezone": timezone,
                    "page_size": page_size,
                    "expected_version": expected_version,
                },
            )
            row = result.mappings().one_or_none()

            if row is None:
                raise ConcurrentPreferenceUpdateError(
                    "Preference changed before the update was applied"
                )

            preference = UserPreference(
                account_id=int(row["account_id"]),
                user_id=int(row["user_id"]),
                timezone=str(row["timezone"]),
                page_size=int(row["page_size"]),
                version=int(row["version"]),
            )

        key = self._key(account_id, user_id)

        try:
            await self._redis.set(
                key,
                json.dumps(asdict(preference)),
                ex=self._ttl_seconds,
            )
        except RedisError:
            # The business update already committed. Remove any old
            # cache value and allow the next read to repopulate it.
            logger.exception(
                "Write-through cache update failed",
                extra={
                    "cache_key": key,
                    "version": preference.version,
                },
            )

            try:
                await self._redis.delete(key)
            except RedisError:
                logger.exception(
                    "Failed to remove stale preference cache",
                    extra={"cache_key": key},
                )

        return preference

The database commits before Redis is updated. A cache failure does not convert a successful durable update into an ambiguous client retry.

A read path can use the same cache while preserving version information:

from __future__ import annotations

import json

from redis.asyncio import Redis
from redis.exceptions import RedisError


class PreferenceReader:
    def __init__(
        self,
        redis: Redis,
        repository: PreferenceRepository,
    ) -> None:
        self._redis = redis
        self._repository = repository

    async def get(
        self,
        account_id: int,
        user_id: int,
    ) -> UserPreference | None:
        key = (
            f"preference:v1:"
            f"account:{account_id}:"
            f"user:{user_id}"
        )

        try:
            cached = await self._redis.get(key)
            if cached is not None:
                return UserPreference(**json.loads(cached))
        except RedisError:
            pass

        preference = await self._repository.get(
            account_id=account_id,
            user_id=user_id,
        )

        if preference is None:
            return None

        try:
            await self._redis.set(
                key,
                json.dumps(asdict(preference)),
                ex=3_600,
            )
        except RedisError:
            pass

        return preference

Advantages, Disadvantages, and Use Cases

Advantages:

  • Frequently updated values remain warm.
  • Reads immediately after writes usually hit the cache.
  • Write coordination is centralized.
  • Cache population does not depend on a later read.
  • The cached representation can include the committed database version.

Disadvantages:

  • Every write performs both database and cache work.
  • Client-facing write latency increases.
  • Values that are never read again still consume cache capacity.
  • Partial failures require clear recovery rules.
  • The cache becomes more closely coupled to the write path.

When to use:

  • user preferences
  • tenant configuration
  • feature settings
  • small entity summaries read immediately after updates
  • data expected to remain consistently warm

When not to use: write-heavy data with low read reuse, large cached objects, or systems where cache latency must never affect the write path.

Write-Behind

Write-behind, also called write-back caching, acknowledges a write before the database is updated. The new state is written to a cache, durable queue, or append-only log, then persisted asynchronously by a worker.

This pattern reduces client-facing latency and allows updates to be batched or combined, but it changes the durability model. The database may lag behind state already acknowledged to the client.

Write and Persistence Flow

Update request
    |
    v
Durable event or write buffer
    |
    +---- update cache
    |
    v
Return success
    |
    v
Background persistence worker
    |
    +---- batch or coalesce updates
    |
    v
Write database

Using volatile cache memory as the only buffer is unsafe. A cache-node failure can lose acknowledged writes before persistence.

A durable log provides replay:

Application
    |
    v
Durable event stream
    |
    +---- cache projection
    |
    +---- persistence worker
              |
              v
           Database

The event stream becomes the recovery source. The cache serves the latest accepted value, while the database eventually receives durable aggregated state.

Python Example

The following example handles shipment-view counters. The workload is high volume, updates are additive, and short persistence delay is acceptable.

from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import Protocol

from redis.asyncio import Redis
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine


logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class ViewIncrement:
    event_id: str
    shipment_id: str
    increment: int


class DurableEventSource(Protocol):
    async def read_batch(
        self,
        maximum_events: int,
    ) -> list[ViewIncrement]:
        ...

    async def acknowledge(
        self,
        event_ids: list[str],
    ) -> None:
        ...


class ViewCounterWriteBehindWorker:
    def __init__(
        self,
        redis: Redis,
        engine: AsyncEngine,
        event_source: DurableEventSource,
    ) -> None:
        self._redis = redis
        self._engine = engine
        self._event_source = event_source

    async def process_batch(
        self,
        maximum_events: int = 1_000,
    ) -> int:
        events = await self._event_source.read_batch(
            maximum_events
        )

        if not events:
            return 0

        increments: dict[str, int] = {}

        for event in events:
            increments[event.shipment_id] = (
                increments.get(event.shipment_id, 0)
                + event.increment
            )

        async with self._engine.begin() as connection:
            for shipment_id, increment in increments.items():
                await connection.execute(
                    text(
                        """
                        INSERT INTO shipment_view_counts (
                            shipment_id,
                            view_count,
                            updated_at
                        )
                        VALUES (
                            :shipment_id,
                            :increment,
                            CURRENT_TIMESTAMP
                        )
                        ON CONFLICT (shipment_id)
                        DO UPDATE
                        SET view_count =
                                shipment_view_counts.view_count
                                + EXCLUDED.view_count,
                            updated_at = CURRENT_TIMESTAMP
                        """
                    ),
                    {
                        "shipment_id": shipment_id,
                        "increment": increment,
                    },
                )

        # Acknowledge only after the database commit succeeds.
        await self._event_source.acknowledge(
            [event.event_id for event in events]
        )

        logger.info(
            "Persisted view-counter batch",
            extra={
                "event_count": len(events),
                "shipment_count": len(increments),
            },
        )
        return len(events)

Blind increments can be duplicated if the worker commits the database transaction but crashes before acknowledging the stream. A stronger implementation stores processed batch identifiers or stream offsets in the same database transaction.

CREATE TABLE write_behind_batches (
    consumer_name TEXT NOT NULL,
    batch_id TEXT NOT NULL,
    processed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (consumer_name, batch_id)
);

BEGIN;

INSERT INTO write_behind_batches (
    consumer_name,
    batch_id
)
VALUES (
    'shipment-view-counter',
    :batch_id
)
ON CONFLICT DO NOTHING
RETURNING batch_id;

-- Continue only when the batch marker was inserted.

INSERT INTO shipment_view_counts (
    shipment_id,
    view_count,
    updated_at
)
VALUES (
    :shipment_id,
    :increment,
    CURRENT_TIMESTAMP
)
ON CONFLICT (shipment_id)
DO UPDATE
SET view_count =
        shipment_view_counts.view_count
        + EXCLUDED.view_count,
    updated_at = CURRENT_TIMESTAMP;

COMMIT;

The cache projection can be updated from the same durable stream:

from __future__ import annotations

from redis.asyncio import Redis


class ViewCounterCacheProjector:
    def __init__(self, redis: Redis) -> None:
        self._redis = redis

    async def apply(
        self,
        event: ViewIncrement,
    ) -> int:
        return int(
            await self._redis.incrby(
                f"shipment-views:v1:{event.shipment_id}",
                event.increment,
            )
        )

Production systems should make cache projection idempotent as well, or rebuild it from database totals and stream checkpoints after failure.

Advantages, Disadvantages, and Use Cases

Advantages:

  • Very low client-facing write latency.
  • Multiple updates can be combined.
  • Traffic spikes can be buffered.
  • Database write amplification can be reduced.
  • Useful for high-frequency additive or replaceable state.

Disadvantages:

  • Acknowledged writes may not yet exist in the database.
  • Volatile buffers can lose data.
  • Backlogs increase persistence delay.
  • Ordering, replay, and duplication require explicit handling.
  • Database validation failures happen after client success.
  • Recovery is considerably more complex.

When to use:

  • view and impression counters
  • telemetry aggregation
  • activity metrics
  • nonfinancial counters
  • frequent updates that can be coalesced
  • state rebuildable from a durable event log

When not to use:

  • payments and financial ledgers
  • inventory reservations
  • security-sensitive state
  • strict uniqueness workflows
  • data requiring immediate relational validation

Strategy Comparison

The main difference is where the system places latency and recovery complexity. Cache-aside optimizes repeated reads, write-through keeps the cache warm synchronously, and write-behind moves database persistence outside the request path.

Property Cache-Aside Write-Through Write-Behind
Read latency Low after warm-up Low because writes populate cache Low for the latest cached state
Write latency Database write plus invalidation Database and cache write Low before asynchronous persistence
Database durability Before success Before success After success
Cache population Lazy Immediate Immediate or event-driven
Stale-read risk Moderate Low when coordinated correctly Cache may be newer than database
Lost-write risk Low Low with database-first writes High without durable buffering
Operational complexity Moderate Moderate High
Best fit General read-heavy systems Frequently read values that should stay warm High-volume delay-tolerant writes

Consistency and Failure Behavior

Cache-aside: database state is durable before invalidation. If deletion fails, stale data remains until invalidation retry or TTL expiration.

Write-through: database state is durable before the cache is refreshed. If cache update fails, the system should remove the old entry or let the next read reload it.

Write-behind: the database can remain behind acknowledged application state. Durable buffering, lag monitoring, idempotent replay, and dead-letter handling are required.

Timeouts are ambiguous in every pattern. A timeout does not prove that a write failed. Idempotency keys, entity versions, batch identifiers, and conditional updates are required for safe retries.

Choosing the Right Strategy

Requirement Preferred Strategy Reason
Database must remain immediately authoritative Cache-aside Writes commit directly to the database
Values are usually read after every write Write-through The committed value immediately warms the cache
Client-facing write latency must be minimized Write-behind Database persistence occurs asynchronously
Strict relational validation is required Cache-aside or database-first write-through Validation completes before success
Many small updates can be combined Write-behind Workers can batch or coalesce operations
Cache must remain optional Cache-aside Cache failure does not need to block writes

Production Design Example

A logistics platform can use all three strategies because shipment details, user preferences, and view counters have different consistency and durability requirements.

Architecture

Shipment details
    |
    +--> PostgreSQL authoritative state
    |
    +--> Redis cache-aside representation


User preferences
    |
    +--> preference service
            |
            +--> PostgreSQL
            +--> Redis write-through cache


Shipment view counters
    |
    +--> durable event stream
            |
            +--> Redis current counter
            +--> batch persistence worker
                    |
                    v
                PostgreSQL

The system applies:

  • Cache-aside to shipment details because PostgreSQL must remain authoritative.
  • Write-through to preferences because they are read on most requests after updates.
  • Write-behind to view counters because updates are additive and can tolerate delayed persistence.

Request and Failure Flows

Shipment update:

  1. PostgreSQL updates shipment state.
  2. The same transaction stores an outbox event.
  3. An invalidation consumer deletes the cache key.
  4. The next read reloads committed state.

Preference update:

  1. PostgreSQL validates the expected version.
  2. The committed representation is written to Redis.
  3. If Redis fails, the key is deleted or allowed to expire.
  4. The database update remains successful.

View-counter update:

  1. The API appends an increment to a durable stream.
  2. A projector updates Redis.
  3. A persistence worker batches increments into PostgreSQL.
  4. Batch identifiers prevent duplicate database increments.

Redis failure: shipment and preference reads fall back to PostgreSQL with bounded concurrency. View events remain in the durable stream until Redis recovers.

Database failure: shipment and preference writes fail because synchronous durability is required. View events continue accumulating until queue retention or operational limits are approached.

Worker failure: write-behind persistence lag grows, but unacknowledged events remain available for replay.

Network timeout: idempotency keys and version checks prevent blind duplicate writes.

Monitoring

Strategy Critical Metrics Primary Risk
Cache-aside Hit ratio, miss rate, load latency, invalidation failures Cache misses overload the database
Write-through Write latency, cache update failures, stale-key deletions Database commits while cache remains stale
Write-behind Queue depth, oldest event age, retries, dead letters Persistence lag exceeds business or retention limits

Cross-strategy monitoring should also include cache command latency, evictions, database fallback traffic, stale-read incidents, duplicate processing, and recovery duration after cache or database failure.

Common Mistakes

Mistake Production Impact Better Approach
Using one strategy for every data type Correctness and latency requirements become mixed Select the strategy per data responsibility
Updating cache before the database commits Uncommitted values can become visible Commit first unless one transactional system coordinates both writes
Returning failure after the database committed Client retries can duplicate the business operation Use idempotency and treat cache refresh as recoverable
Using volatile Redis memory as the write-behind log A cache failure loses acknowledged writes Use a durable queue or append-only event stream
Ignoring event ordering Older values overwrite newer state Use versions, sequence numbers, or ordered partitions
Using cache-aside without request coalescing Popular expired keys create database spikes Use per-key locks or distributed coalescing
Directly reconstructing complex cache entries after writes Generated and joined fields can be missing Delete the key and rebuild from committed state
Using write-through for low-reuse data Cache capacity and write throughput are wasted Warm only data with proven read demand
Running write-behind without backlog limits Lag grows until recovery becomes impractical Alert on depth, age, throughput, and retention headroom
Using write-behind for payments or inventory Accepted writes may fail or disappear later Persist critical state synchronously
Omitting finite TTLs Failed invalidations leave stale values indefinitely Use expiration as a final recovery boundary
Monitoring only hit ratio Write lag and partial failures remain hidden Monitor read, write, invalidation, and persistence paths

Production Checklist

  • Define the authoritative store for every cached value.
  • Select a caching strategy for each workload independently.
  • Commit authoritative database writes before exposing cached state.
  • Use idempotency keys and entity versions.
  • Keep finite TTLs even with explicit invalidation.
  • Coalesce concurrent cache-aside misses.
  • Use durable buffering for acknowledged write-behind operations.
  • Prevent stale events from overwriting newer versions.
  • Define recovery for partial database and cache success.
  • Alert on invalidation and cache-update failures.
  • Alert on write-behind queue age and depth.
  • Load-test cache outages and cold-cache recovery.
  • Test duplicate delivery and worker restart behavior.
  • Protect the database with bounded fallback concurrency.
  • Keep financial, inventory, and security-critical writes synchronous.

Conclusion

Cache-aside is the safest general-purpose pattern when the database must remain authoritative. Write-through keeps frequently read data warm but adds cache work to synchronous writes. Write-behind reduces write latency and database pressure but requires durable buffering, ordering, replay, and lag management.

Key Takeaway: Use cache-aside for authoritative read-heavy data, write-through when committed values should immediately warm the cache, and write-behind only when delayed database persistence is acceptable and every acknowledged write is protected by a durable replayable log.

Author

Enjoyed this article?

Support Oleksandr Andrushchenko

Buy me a coffee

This helps Oleksandr Andrushchenko continue creating useful content

Related articles

Comments (0)