Model business invariants before endpoints
In a mature Django application, an invariant is a condition that must remain true after every committed operation. Examples include no duplicate license for the same paid entitlement, no negative physical stock where backorders are forbidden, and no publishing without editorial approval. Encode unconditional constraints at the database layer when possible: unique constraints, check constraints and foreign keys. Request serializers or forms provide useful early feedback, but they are not substitutes for database integrity when concurrent transactions race. A correctness rule that exists only in a template is not protected.
Explicit application services reduce accidental coupling
A service function should implement a named operation such as publish_article or reserve_inventory, receiving validated inputs and an actor identity. The view handles HTTP status, content negotiation and headers; the service checks authorization and invariants; models persist validated state. Avoid a collection of gigantic model.save overrides that conceal network requests. Sending emails and triggering remote APIs inside a model method makes retries and tests difficult. Return a typed result or raise documented domain errors so callers can distinguish malformed input, forbidden access, conflict and transient infrastructure failure.
Transaction atomicity and concurrent updates
Use transaction.atomic to group changes that must succeed or fail together. For contended rows, a database capable of row locking may support select_for_update inside that transaction; SQLite has different locking behavior, so do not infer PostgreSQL guarantees from a local SQLite test. For example, before decrementing a stock row, read or lock the record, verify available quantity, write the result, and commit atomically. Unique constraints are a final defense against simultaneous inserts. Write explicit concurrency tests on the target production database engine.
External side effects and the outbox pattern
The database cannot atomically commit alongside arbitrary external email or payment APIs. transaction.on_commit can prevent a callback from running before a successful local commit, but a process crash after commit may still lose the external call. An outbox table recorded within the transaction gives a durable queue of intended events. A separate worker retries dispatch with stable event identifiers, and the receiver must handle duplicates. A payment process needs distinct states such as pending, observed, confirmed, settled, canceled and refunded, each with guards against invalid transitions.
Validate boundary inputs precisely
Use Django forms, serializers or explicit validators to check type, allowed range, format and cross-field relationships. Do not trust hidden inputs, JavaScript calculations or client-declared user IDs. A management endpoint should check object-level permission against the current authenticated actor, not merely whether someone is logged in. For file uploads, validate actual media type, storage destination, extension, size and intended processing path. Use context-aware escaping in templates and prohibit user-supplied HTML unless it passes a specialized sanitizer with an explicit policy.
Production settings and security controls
Keep secrets outside source control; rotate keys with a documented recovery path. Disable DEBUG in production; define ALLOWED_HOSTS and trusted proxy behavior deliberately; configure secure session and CSRF cookies for HTTPS. Review redirects, host-header handling, static-file storage, cache backend and log redaction. Treat manage.py check --deploy as a preflight aid, not certification. The reverse proxy and application may disagree about scheme and forwarded headers; confirm secure-request detection in the actual deployment topology. HSTS must be planned with domain and subdomain consequences in mind.
Migrations, static assets and rollback
For nontrivial schema changes, consider expand-migrate-contract: first deploy backward-compatible columns or tables, backfill safely, then remove old fields after the application no longer needs them. Do not combine an irreversible destructive migration with a routine cosmetic release. Check collectstatic manifests and asset paths before switching traffic. Retain database backups separately from code backups; a template rollback cannot reverse a schema change. Test a deployment against realistic data volume and monitor errors after cutover before declaring it healthy.
A verification-oriented Django release checklist
Test model constraints, role-specific permissions, concurrent writes, duplicate webhook delivery, cache behavior and actual HTTP routes. A clean CI run on a test database proves only those conditions executed. In staging, test deployment with DEBUG disabled and representative settings. In production, check home and deep links, admin login, static assets, payment isolation and rollback readiness. Publish a clear operational runbook with what changed, how success is measured and what triggers an immediate revert. References: https://docs.djangoproject.com/en/5.2/topics/db/transactions/ and https://docs.djangoproject.com/en/5.2/topics/security/ .
Editorial disclosure: Original advanced educational content by Mojtaba Roozegar. Examples are illustrative, not an audit of a particular production deployment or a guarantee of outcomes. Relevant external technical references are linked within each article.
