CQRS in Django: a denormalized read model without Event Sourcing

CQRS in Django: a denormalized read model without Event Sourcing

The four previous articles in the series laid the building blocks for a distributed system to stay consistent: Saga to orchestrate workflows, Outbox to publish reliable events, Inbox to consume them without duplicates, Idempotency Keys to protect the API. One question is left: what do you do with events once they are published? The most common answer: you use them to build read views. That is exactly what the CQRS pattern (Command Query Responsibility Segregation) proposes: separate the write model from the read model when both diverge enough that forcing them into a single structure costs more than splitting them. ...

June 5, 2026 · 7 min · Anthony
Idempotency Keys: stopping a client from paying twice

Idempotency Keys: stopping a client from paying twice

The previous two articles tackled idempotency on the event side: the Outbox pattern guarantees a message is published at least once, and the Inbox pattern guarantees it is consumed only once. One last place where the same problem shows up sits further upstream: the HTTP API itself. When a client fires a POST /api/payments and the connection drops before the response comes back, the client has no way to know whether the payment was created. If it retries, it risks paying twice. If it does not retry, it risks not paying at all. The Idempotency Key pattern, popularized by Stripe and adopted since by most payment APIs, solves that dilemma by putting retry control in the client’s hands. ...

June 4, 2026 · 7 min · Anthony
Inbox pattern: consuming events without replaying them twice

Inbox pattern: consuming events without replaying them twice

The previous article on the Transactional Outbox set a clear guarantee: every event written to the database will eventually be published. That guarantee is intentionally at-least-once. A consumer may receive the same event two times, three times, or more if the network behaves badly. The Outbox pattern never promises uniqueness. The consequence follows immediately: if the consumer applies the effect of the message twice, it bills twice, sends two emails, decreases stock twice. The consistency guaranteed on the producer side collapses on the reader side. ...

June 3, 2026 · 6 min · Anthony
Transactional Outbox: publishing events without losing consistency

Transactional Outbox: publishing events without losing consistency

When a service updates its database and wants to notify the rest of the system by emitting an event to Kafka, RabbitMQ or SQS, the naive code looks like this: write to the database, then publish. If publishing fails after the commit, the event is lost. If publishing succeeds but the commit fails, the event refers to a state that does not exist. Both cases define the dual-write problem. The Transactional Outbox pattern fixes that inconsistency with a simple idea: never publish directly. The event is written to an outbox table within the same SQL transaction as the business change. A separate process reads that table and publishes to the broker. As long as the SQL transaction is atomic, the database and the future event are consistent by construction. ...

June 2, 2026 · 7 min · Anthony
Saga pattern: handling distributed transactions without rollback

Saga pattern: handling distributed transactions without rollback

A business operation that spans several services raises a question SQL has been answering for fifty years inside a single database: what happens when one step succeeds and the next one fails? As long as everything lives in the same database, BEGIN ... ROLLBACK is enough. The moment you call an external service, a third-party API or another database, that safety net disappears. The Saga pattern answers that question. Rather than attempting an impossible ACID transaction, it breaks the operation into local steps, each paired with a compensating transaction that knows how to undo its effect. If step 4 fails, the compensations for steps 1, 2 and 3 are replayed in reverse order. ...

June 1, 2026 · 7 min · Anthony
Declarative Permissions in DRF with rest_access_policy

Declarative Permissions in DRF with rest_access_policy

Django REST Framework permissions work, but they show their limits as soon as access rules get moderately complex. Multiple roles, objects belonging to a specific user, custom actions on a ViewSet: you end up with has_permission and has_object_permission classes mixing heterogeneous checks, hard to read and even harder to test. rest_access_policy (package djangorestframework-access-policy) takes a different approach: declare access rules as statements, similar to AWS IAM policies. The result is readable at a glance, testable independently of the ViewSet, and extensible without rewriting the entire class. ...

May 26, 2026 · 6 min · Anthony
Hash, HMAC and encryption: securing a Django token

Hash, HMAC and encryption: securing a Django token

A == comparison on a hash is not enough to pick the right mechanism. sha256, HMAC, salted hash, encryption: each approach offers different guarantees. Understanding which ones changes concretely how you store and verify a token in Django. Simple hash import hashlib token_hash = hashlib.sha256(token.encode()).hexdigest() A simple hash is deterministic: the same input always produces the same output. No server secret is involved. It is impossible to recover the original token from the hash (sha256 is a one-way function). But if someone knows or guesses the token, they can recompute the hash and compare. ...

May 25, 2026 · 4 min · Anthony
Django: save() skips full_clean() — the validation lifecycle

Django: save() skips full_clean() — the validation lifecycle

Calling obj.save() after defining validators and a clean() method on the model gives the impression that validation is guaranteed. It is not. Django does not call full_clean() during a save(), and this behavior is intentional. Understanding why changes how you architect validation in a project. What save() actually does The lifecycle of a save() call is shorter than you might expect: pre_save signal sent field.pre_save() called on each field (auto_now, auto_now_add, etc.) INSERT or UPDATE SQL based on whether a pk exists post_save signal sent No validation appears anywhere in this sequence. No blank check, no max_length, no call to clean(). The same applies to Model.objects.create() and Model.objects.bulk_create(): all three methods persist without validating. (bulk_create() is covered in depth in Django in_bulk() and bulk_create() if you work with bulk inserts.) ...

May 18, 2026 · 5 min · Anthony
Timing attacks in Django with constant_time_compare

Timing attacks in Django with constant_time_compare

A == comparison on a token looks harmless. In practice, it leaks a measurable piece of information: execution time varies depending on how many characters match. That is the principle behind a timing attack, and it is enough for an attacker to reconstruct the token one character at a time. The problem: the comparison that stops too early Python compares strings character by character and stops as soon as a mismatch is found. ...

May 14, 2026 · 3 min · Anthony
Materialized views vs Django cache for slow queries

Materialized views vs Django cache for slow queries

The instinctive response to a slow reporting endpoint is often cache. A @cache_page, a cache.set(), and the problem seems to vanish until the next expiration. This approach has a structural limitation that PostgreSQL materialized views solve at the root. The problem with cache on analytics endpoints Django cache stores the result of a Python view. The expensive SQL query still runs on every cache expiration. For a report built on multiple JOINs and aggregations, this means the first user after each cache miss waits several seconds. ...

May 13, 2026 · 4 min · Anthony

Newsletter

Get new articles delivered straight to your inbox.

No spam. Unsubscribe in one click.