consistency-patterns-knowledge
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 componentreferences/idempotency-patterns.md— Idempotency key generation, deduplication store, PSR-15 middleware, delivery guarantees, non-idempotent operations
More from dykyi-roman/awesome-claude-code
psr-overview-knowledge
PHP Standards Recommendations (PSR) overview knowledge base. Provides comprehensive reference for all accepted PSRs including PSR-1,3,4,6,7,11,12,13,14,15,16,17,18,20. Use for PSR selection decisions and compliance audits.
22detect-code-smells
Detects code smells in PHP codebases. Identifies God Class, Feature Envy, Data Clumps, Long Parameter List, Long Method, Primitive Obsession, Message Chains, Inappropriate Intimacy. Generates actionable reports with refactoring recommendations.
15clean-arch-knowledge
Clean Architecture knowledge base. Provides patterns, antipatterns, and PHP-specific guidelines for Clean Architecture and Hexagonal Architecture audits.
15ddd-knowledge
DDD architecture knowledge base. Provides patterns, antipatterns, and PHP-specific guidelines for Domain-Driven Design audits.
14testing-knowledge
Testing knowledge base for PHP 8.4 projects. Provides testing pyramid, AAA pattern, naming conventions, isolation principles, DDD testing guidelines, and PHPUnit patterns.
12bug-root-cause-finder
Root cause analysis methods for PHP bugs. Provides 5 Whys technique, fault tree analysis, git bisect guidance, and stack trace parsing.
12