Cache invalidation, async boundaries, and the work behind novademo.tech.
During the deployment of Django Nova’s public demo, the application was already returning {"status": "ok"}. PostgreSQL, Redis, Memcached, and the web container were healthy.
The same health endpoint through Nginx returned 404.
That small failure was a useful reminder: a successful check proves something about the layer it reaches. It does not automatically prove that the next layer works.
I have been learning the same lesson inside Django Nova.
In my earlier architecture article, I focused on duplicated validation and shared schemas. In the typing follow-up, I described the difficulty of putting a typed interface around Django’s dynamic metadata.
Now there is a public application where readers can inspect and exercise some of those ideas:
Getting there meant dealing with transactions, mutable cached objects, asynchronous execution, deployment, and recovery. It also meant being more precise about what Nova promises.
A shared schema still needs boundaries
Django Nova connects Django models with Pydantic schemas, query planning, caching, and optional integrations. Django’s ORM remains responsible for persistence.
The original motivation is familiar: an application can repeat similar rules in a serializer, a form, a model, and a background task. As these entry points evolve, their behavior can drift.
Shared schemas help, but the different validation layers still have work to do.
In the current implementation, NovaModel.save() checks the selected Pydantic schema, validates and converts Django fields, runs Model.clean(), checks uniqueness and model constraints, and then saves through Django. A failure stops the later stages.
One detail matters more than it initially appears: a value returned by Django’s field.clean() must be assigned back to the instance before model-level validation runs. Otherwise, a later validator can inspect the original value even though field conversion succeeded.
There are also write paths that bypass this sequence. QuerySet.update(), bulk_create(), and bulk_update() do not call model save(). Database constraints remain necessary, particularly when writes race.
This is a qualification I would add to the stronger language in my earlier article. The useful guarantee is a defined validation path with documented exceptions. Sharing a schema does not make every possible database write pass through it.
The validation guide describes that contract.
Types helped expose assumptions
One failure from the typing work still captures the problem well.
I had treated a primary key as a field that must pass Nova’s general-purpose concrete-field filter. Django already had an authoritative answer in _meta.pk. My abstraction was imposing an extra condition on it.
The fix was to preserve Django’s definition at that boundary.
The subsequent runtime work exposed other assumptions: callable defaults evaluated too early, file objects needing a filename representation, and serialization touching relations that the selected schema did not request.
These are observable behaviors. A type checker can help organize the code around them, but regression tests need to establish what actually happens.
Even a clean Pyright result has a scope. Nova’s configuration contains exclusions and adjusted diagnostics. It should be read alongside those settings, rather than as a claim that every integration has identical static guarantees.
Caching forced me to think about time and ownership
Caching produced some of the most useful engineering work because a successful cache hit answers so few of the difficult questions.
Consider invalidation during a transaction. A model save may succeed and the surrounding transaction may still roll back. Invalidating on the save signal alone does not establish the right relationship with committed data.
Nova’s signal-driven invalidation is deferred until the relevant database transaction commits. Rollback discards the callback. Reads inside a transaction use the database.
The transaction tests make the timing concrete: they cover commit, rollback, nested savepoints, and database aliases. Their recording cache is intentional; those tests check when invalidation is requested, while backend tests cover the transport.
Then there is a harder race:
- A reader starts fetching an older database result.
- A writer commits a change.
- The writer invalidates the cached query.
- The original reader finishes and tries to cache its older result.
Deleting an existing key does not, by itself, stop that final write.
The shared-generation approach associates results with tokens scoped to a model and database alias. A fill keeps the generation captured before its SQL query. After a successful generation change, a late result cannot become an entry in the new generation.
The regressions also cover a writer that has never populated the reader’s cache.
This still does not create an atomic transaction between PostgreSQL and Redis or Memcached. If a writer cannot deliver invalidation during a network failure, another process may retain stale data. That limitation needs an application-specific consistency policy.
A separate issue concerns ownership of the returned objects.
Suppose one caller reads cached models and changes a nested JSON value without saving. A later caller must not observe that in-memory edit as if it came from the database.
The same problem applies to the list itself, model attributes, and already-loaded related objects. Copying only the outer list is insufficient.
Nova’s result-isolation tests mutate those structures and then check that another read preserves the stored result without falling back to SQL.
Optimizing those copies requires another distinction: a backend that returns independent values on reads does not necessarily capture independent values on writes. The cache contract treats those as separate capabilities.
Each of these details adds a question that a simple “miss, then hit” example cannot answer.
Async behavior needs its own evidence
An async function introduces another kind of boundary: the difference between creating a coroutine and executing it.
A tracing wrapper can open a span, call an async function, receive its coroutine, and close the span before the actual work runs. It has measured coroutine creation while missing the awaited operation.
Nova’s tracing decorators were changed to keep the span open across the await. The tests verify execution order, exceptions raised after suspension, and cancellation propagation.
Serialization raises a different async concern. Reading an unloaded foreign key can execute synchronous SQL. An async save API does not automatically make every subsequent access to that model safe inside an event loop.
I want those distinctions visible in the documentation and the demo. Developers need to know which operation runs, what it waits for, and what could still touch the database.
What you can try at novademo.tech
The demo is an application built with Nova, with a product catalog and an interactive lab. It includes a browser-session workspace, schema inspection, API documentation, and Russian and English interfaces.
Its scenario registry currently defines 19 scenarios. They cover validation, serialization, query planning, cache behavior, transactions, result isolation, context handling, tasks, adapters, and infrastructure integrations.
If you have five minutes, I suggest starting with:
- Validation: change a product’s name or price and inspect the result.
- Query planning: compare the SQL counts for the same data and relationships.
- Commit and rollback: inspect when a changed value becomes visible.
- Result isolation: see whether an unsaved mutation affects another cached read.
The lab also exposes its limits. The task example runs in-process and finishes within the request. The migration scenario previews SQL without executing DDL. GraphQL is an experimental, restricted read-only example. These distinctions are recorded in the capability map.
The deployment uses one Gunicorn worker and one thread because several lab demonstrations involve process-local state. It gives the examples a controlled execution environment; evaluating a deployment with multiple workers requires additional work.
The displayed timings include instrumentation. They help explain a scenario, but they are not a benchmark establishing general performance advantages.
Hosting made the operational work visible
I deployed the site on a Hostinger VPS using Docker Compose, with PostgreSQL, Redis, and Memcached behind the application. Nginx handles public traffic and HTTPS.
The setup involved several separate checks. An unwanted AAAA record remained after the IPv4 configuration, so I checked the authoritative DNS servers and a public resolver after removing it. The application’s local health endpoint worked before Nginx routed the domain correctly. Certificate issuance succeeded, and the renewal dry run passed.
That process made the diagnostic order clearer: application, reverse proxy, DNS, then HTTPS. Checking each layer separately made the failures easier to locate.
Backups required their own definition of success.
The backup process briefly pauses the web application while PostgreSQL and uploaded files are copied. It resumes the site, checks the archives and checksums, and restores the database dump into a temporary database.
I also copied the first completed backup to another device and checked its hashes there.
That establishes more than the existence of an archive. It still leaves work to do: automatic off-server copying is not configured, and a complete recovery of the entire VPS has not been rehearsed. The temporary-database restore is a specific check, with a specific scope.
The next useful feedback is a concrete case
The demo pins the published django-nova==0.6.3 package. The main repository and its documentation can evolve separately, so examples should always be checked against the installed release.
Nova remains beta. The current package declares Python 3.12 or newer, Django >=5.0,<6.0, and Pydantic >=2.8,<3.0. Those dependency bounds do not prove every combination has been tested.
My aim for the public demo is to make discussion more concrete. Readers can start with a behavior, inspect its limits, and compare it with an application they know.
I would especially welcome cases involving related-object serialization, writes that bypass save hooks, and cached queries with frequent updates. A small model, schema, and failing operation would make useful feedback.
Which behavior would you need to verify before introducing something like Nova into your Django application?
Try the demo: novademo.tech
Explore the library: Artem7898/django-nova
Inspect the demo source: Artem7898/nova-demo
Documentation:Django Nova site

Top comments (4)
hello bro
Some comments may only be visible to logged-in visitors. Sign in to view all comments.