Django select_related() vs prefetch_related(): SQL explained

Django select_related() vs prefetch_related(): SQL Explained

Django select_related() or prefetch_related()? Both methods eliminate N+1 queries in Django, but they produce very different SQL. The first adds a join to the main query. The second runs multiple queries, then connects the objects in Python. The right choice depends less on data volume than on the type of relationship being traversed. To understand why, let’s start with a deliberately simple model: from django.conf import settings from django.db import models class Post(models.Model): title = models.CharField(max_length=200) author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="posts", ) tags = models.ManyToManyField("Tag", related_name="posts") class Comment(models.Model): post = models.ForeignKey( Post, on_delete=models.CASCADE, related_name="comments", ) body = models.TextField() is_public = models.BooleanField(default=True) author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) class Tag(models.Model): name = models.CharField(max_length=50) From a Post, author refers to at most one user. By contrast, comments and tags can contain multiple objects. This difference in cardinality almost always determines which method to use. ...

July 20, 2026 · 8 min · Anthony
GitOps Without Kubernetes: Django with doco-cd, SOPS and age

GitOps Without Kubernetes: Django with doco-cd, SOPS and age

GitOps is almost always presented with Kubernetes in the background: ArgoCD, Flux, manifests by the hundreds. As a result, many teams deploying on a plain VPS with Docker Compose assume the model does not apply to them. They keep deploying over SSH: git pull, docker compose up -d, crossing their fingers that nobody skips a step. That is a shame, because the core of GitOps has nothing to do with Kubernetes. And for some time now, one tool has been filling exactly this gap: doco-cd, a continuous deployment engine for Docker Compose. Combined with SOPS and age to encrypt secrets directly in the repo, you get GitOps without Kubernetes: a complete pipeline where deploying boils down to git push. This article explains the principle, then walks through the setup on a Django project with several compose files. ...

June 29, 2026 · 10 min · Anthony
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

Newsletter

Get new articles delivered straight to your inbox.

No spam. Unsubscribe in one click.