Consistency Patterns Knowledge Base
Quick reference for consistency guarantees, locking strategies, idempotency, and conflict resolution in distributed PHP applications.
Strong vs Eventual Consistency
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONSISTENCY DECISION TREE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Is data safety-critical? │
│ (money, inventory) │
│ / \ │
│ YES NO │
│ / \ │
│ Single write Can tolerate │
│ location? stale reads? │
│ / \ / \ │
│ YES NO YES NO │
│ | | | | │
│ ▼ ▼ ▼ ▼ │
│ ┌───────┐ ┌────────┐ ┌────────┐ ┌──────────┐ │
│ │Strong │ │Saga + │ │Eventual│ │Lineariz- │ │
│ │(ACID) │ │Compen- │ │Consist.│ │able │ │
│ │ │ │sation │ │+ TTL │ │Reads │ │
│ └───────┘ └────────┘ └────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| Property |
Strong Consistency |
Eventual Consistency |
| Guarantee |
All reads return latest write |
Reads may return stale data temporarily |
| Latency |
Higher (synchronous coordination) |
Lower (async propagation) |
| Availability |
Lower (CAP theorem trade-off) |
Higher (tolerates partitions) |
| Scalability |
Limited by coordination |
Highly scalable |
| Complexity |
Simpler app logic, harder infra |
Simpler infra, harder app logic |
| Use Cases |
Financial transactions, inventory |
Social feeds, analytics, caches |
| PHP Pattern |
DB transactions, SELECT FOR UPDATE |
Event-driven sync, CQRS read models |
Idempotency Keys
Overview
Idempotency ensures that performing the same operation multiple times produces the same result. Critical for payment processing, message handling, and API retries.
| Aspect |
Details |
| What |
Unique key identifying a specific operation attempt |
| Why |
Safe retries, at-least-once delivery semantics, duplicate prevention |
| Format |
UUIDv4 or {client-id}:{operation}:{unique-ref} |
| Storage |
Redis (fast, TTL) or DB (durable, queryable) |
| TTL |
24-72 hours depending on retry window |
Delivery Guarantees
| Guarantee |
Description |
Idempotency Needed? |
| At-most-once |
Fire and forget, may lose messages |
No |
| At-least-once |
Retries until ACK, may duplicate |
Yes |
| Exactly-once |
Process exactly once (hard) |
Built-in deduplication |
Idempotency Middleware (PSR-15)
<?php
declare(strict_types=1);
namespace Infrastructure\Http\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final readonly class IdempotencyMiddleware implements MiddlewareInterface
{
private const string HEADER = 'Idempotency-Key';
public function __construct(
private \Redis $redis,
private int $ttlSeconds = 86400,
) {}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler,
): ResponseInterface {
if ($request->getMethod() === 'GET') {
return $handler->handle($request);
}
$key = $request->getHeaderLine(self::HEADER);
if ($key === '') {
return $handler->handle($request);
}
$cacheKey = sprintf('idempotency:%s', $key);
$cached = $this->redis->get($cacheKey);
if ($cached !== false) {
return unserialize($cached, ['allowed_classes' => true]);
}
$response = $handler->handle($request);
$this->redis->setex($cacheKey, $this->ttlSeconds, serialize($response));
return $response;
}
}
Optimistic Locking
Assumes conflicts are rare. Reads a version, performs work, writes only if version unchanged.
| Component |
Description |
| Mechanism |
Version column incremented on each update |
| Conflict Detection |
WHERE version = :expected in UPDATE |
| On Conflict |
Throw exception, retry or return error |
| Best For |
Read-heavy workloads, low contention |
| Doctrine |
#[ORM\Version] attribute on integer/datetime column |
Doctrine Versioned Entity
<?php
declare(strict_types=1);
namespace Domain\Model;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'products')]
final class Product
{
#[ORM\Id]
#[ORM\Column(type: 'string', length: 36)]
private string $id;
#[ORM\Column(type: 'string', length: 255)]
private string $name;
#[ORM\Column(type: 'integer')]
private int $stock;
#[ORM\Version]
#[ORM\Column(type: 'integer')]
private int $version = 1;
public function __construct(string $id, string $name, int $stock)
{
$this->id = $id;
$this->name = $name;
$this->stock = $stock;
}
public function decrementStock(int $quantity): void
{
if ($this->stock < $quantity) {
throw new InsufficientStockException($this->id, $this->stock, $quantity);
}
$this->stock -= $quantity;
}
public function getVersion(): int
{
return $this->version;
}
}
Handling Optimistic Lock Failures
<?php
declare(strict_types=1);
namespace Application\UseCase;
use Doctrine\ORM\OptimisticLockException;
final readonly class DecrementStockUseCase
{
public function __construct(
private ProductRepositoryInterface $repository,
private int $maxRetries = 3,
) {}
public function execute(string $productId, int $quantity): void
{
$attempt = 0;
while ($attempt < $this->maxRetries) {
try {
$product = $this->repository->findById($productId);
$product->decrementStock($quantity);
$this->repository->save($product);
return;
} catch (OptimisticLockException) {
$attempt++;
if ($attempt >= $this->maxRetries) {
throw new ConcurrencyConflictException($productId);
}
usleep(random_int(10_000, 50_000));
}
}
}
}
Pessimistic Locking
Assumes conflicts are likely. Acquires a lock before reading, blocks other transactions.
| Lock Type |
SQL |
Behavior |
| Exclusive (FOR UPDATE) |
SELECT ... FOR UPDATE |
Blocks reads and writes |
| Shared (FOR SHARE) |
SELECT ... FOR SHARE |
Allows reads, blocks writes |
| NOWAIT |
SELECT ... FOR UPDATE NOWAIT |
Fails immediately if locked |
| SKIP LOCKED |
SELECT ... FOR UPDATE SKIP LOCKED |
Skips locked rows (queue pattern) |
Deadlock Prevention Rules
- Always acquire locks in consistent order (e.g., by entity ID ascending)
- Use lock timeouts (
SET innodb_lock_wait_timeout = 5)
- Keep transactions short (< 1 second)
- Avoid user interaction within transactions
Conflict Resolution Strategies
| Strategy |
Description |
Consistency |
Complexity |
| Last Write Wins (LWW) |
Latest timestamp wins |
Weak |
Low |
| First Write Wins |
First write preserved, reject subsequent |
Strong |
Low |
| Merge Function |
Custom logic merges conflicting writes |
Application-defined |
High |
| CRDTs |
Conflict-free Replicated Data Types |
Eventual (mathematically guaranteed) |
Medium |
| Manual Resolution |
Human decides on conflict |
Strongest |
Variable |
CRDT Basics
| CRDT Type |
Operations |
Example Use |
| G-Counter |
Increment only |
Page view counter |
| PN-Counter |
Increment and decrement |
Inventory count |
| G-Set |
Add only |
Tag collection |
| OR-Set |
Add and remove |
Shopping cart items |
| LWW-Register |
Last write wins register |
User profile fields |
Saga Compensation vs Distributed Transactions
| Aspect |
Distributed Transaction (2PC) |
Saga Pattern |
| Coordination |
Transaction coordinator |
Choreography or orchestration |
| Locking |
Holds locks across services |
No cross-service locks |
| Consistency |
Strong (ACID) |
Eventual (compensating actions) |
| Availability |
Lower (blocking) |
Higher (non-blocking) |
| Complexity |
Simpler logic, complex infra |
Complex logic, simpler infra |
| Failure Handling |
Rollback |
Compensating transactions |
| Latency |
Higher (2-phase commit) |
Lower (async steps) |
| Scalability |
Poor (locks span services) |
Good (independent services) |
Redis Atomic Operations
<?php
declare(strict_types=1);
namespace Infrastructure\Lock;
final readonly class RedisAtomicOperations
{
public function __construct(
private \Redis $redis,
) {}
public function acquireLock(string $resource, string $token, int $ttlMs): bool
{
return $this->redis->set(
sprintf('lock:%s', $resource),
$token,
['NX', 'PX' => $ttlMs],
) === true;
}
public function releaseLock(string $resource, string $token): bool
{
$script = <<<'LUA'
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
return $this->redis->eval(
$script,
[sprintf('lock:%s', $resource), $token],
1,
) === 1;
}
public function compareAndSwap(string $key, string $expected, string $newValue, int $ttl): bool
{
$script = <<<'LUA'
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("setex", KEYS[1], ARGV[3], ARGV[2])
else
return 0
end
LUA;
return $this->redis->eval(
$script,
[$key, $expected, $newValue, (string) $ttl],
1,
) !== 0;
}
}
Common Violations Quick Reference
| Violation |
Where to Look |
Severity |
| No locking on shared mutable state |
Repositories, aggregates |
Critical |
| Missing idempotency on payment endpoints |
API controllers, handlers |
Critical |
| Optimistic lock exception swallowed silently |
Catch blocks, use cases |
Critical |
| Long-running pessimistic locks |
Transaction scopes |
Warning |
| No retry on optimistic lock failure |
Use cases, command handlers |
Warning |
| Distributed transaction across services |
Service-to-service calls |
Critical |
| Missing version field on aggregates |
Doctrine entities |
Warning |
| Idempotency key without TTL |
Redis/cache stores |
Warning |
Detection Patterns
# Optimistic locking
Grep: "@Version|ORM\\\\Version|version" --glob "**/Entity/**/*.php"
Grep: "OptimisticLockException|StaleObjectStateException" --glob "**/*.php"
Grep: "LOCK_OPTIMISTIC|LockMode::OPTIMISTIC" --glob "**/*.php"
# Pessimistic locking
Grep: "FOR UPDATE|FOR SHARE|LOCK_PESSIMISTIC" --glob "**/*.php"
Grep: "LockMode::PESSIMISTIC|SKIP LOCKED|NOWAIT" --glob "**/*.php"
# Idempotency patterns
Grep: "Idempotency|idempotency.key|IdempotencyKey" --glob "**/*.php"
Grep: "deduplication|dedup|duplicate.check" --glob "**/*.php"
# Distributed locks
Grep: "SETNX|NX.*PX|redis.*lock|Lock::acquire" --glob "**/*.php"
Grep: "Redlock|RedisLock|LockFactory" --glob "**/*.php"
# Saga patterns
Grep: "compensat|SagaStep|SagaOrchestrator" --glob "**/*.php"
Grep: "CompensatingAction|rollback|undo" --glob "**/Saga/**/*.php"
# Consistency indicators
Grep: "EventualConsistency|ReadModel|Projection" --glob "**/*.php"
Grep: "transaction|beginTransaction|commit|rollback" --glob "**/*.php"
References
For detailed information, load these reference files:
references/locking-patterns.md — Optimistic locking, pessimistic locking, distributed locks, Redlock algorithm, Symfony Lock component
references/idempotency-patterns.md — Idempotency key generation, deduplication store, PSR-15 middleware, delivery guarantees, non-idempotent operations