Your Domain Model Is the Architecture
One source of truth, an always-valid domain, and why both matter more now that machines write the code.
The drawing and the code
You work the model out on a whiteboard. Aggregates, the events between them, the boundaries around them. It is clear, and everyone in the room agrees on what the system does.
Then you open the editor and the drawing starts to come apart. The ORM wants tables, so the aggregate becomes rows. Validation turns into something you remember to call. Infrastructure details work their way into the business logic. A few months later, the drawing and the code describe two different systems, and the drawing is the one nobody trusts anymore.
The deeper version of this problem is that you end up describing the same system many times. Add one field to an Order and count the places you touch to do it honestly: the model, the migration, the request and response schemas, the API specification, the generated client types the frontend imports, the documentation, the test fixtures, the event other services consume and its version. If the change alters how the system runs, the infrastructure too. Each of those is a separate description of one fact. They agree on the day you write them, and then they drift, because nothing holds them together except attention, and attention is finite.
We treat this as the normal cost of building software. It is a design choice we stopped noticing.
One idea: the domain model is the architecture
Protean starts from a single claim. Your domain model is not configuration for a database, and it is not an input to an API layer. It is the architecture. You write it in Python the way you drew it, and everything else the system needs is derived from it.
The mechanism is one rule: anything that can be derived from the model should be derived from it, and never maintained by hand. A hand-maintained copy of something the model already knows is a bug waiting for the moment it drifts.
The industry has already lived this once, on the frontend. Before React, you changed the DOM by hand to match your data, and the two drifted constantly. The recurring bug always had the same shape: the screen does not match the state. React’s move was to stop maintaining the copy. You declare the state, and the view is derived from it. The relief was immediate and permanent, and nobody who felt it wants to go back to hand-syncing the DOM.
The same move applies to the whole system behind the screen. Declare the domain model once. Derive the documentation, the API specification, the client types, the event contracts, and the infrastructure from it. The model is the state; everything downstream is the view. You maintain one thing by hand, and the drift has nowhere to enter.
For derivation to be possible, a framework has to understand your domain
This is the part that is genuinely hard, and it is why almost no framework does it.
To derive documentation, an API specification, and infrastructure from a model, a framework needs complete and structured knowledge of that model: the whole shape of it. Which aggregates exist, how they relate, which commands and events flow between them, what constrains them, what infrastructure they imply. Most frameworks are runtime-only. They execute your code without understanding what it means. Django knows your models. A web framework knows your routes. Neither knows your domain as a whole, so neither can derive anything from it past the narrow slice it tracks.
Protean parses your domain into an Intermediate Representation: a portable JSON document that captures
the topology of the model after domain.init() runs. The name is borrowed on purpose. A compiler
parses source once into a structured form, and every stage after that reads the structured form
instead of re-reading the text. The IR is that structured form for a domain. It records what exists
and how it connects, and it records this deterministically, so the same model always produces
byte-identical IR. That property is what makes the IR diffable: two versions of a model can be
compared to see exactly what changed, in a way a pile of Python source never could be.
One discipline keeps the IR honest. It captures topology, never logic. It records that an aggregate
has a post-invariant named order_must_have_items and when that guard runs, and it does not record
what the guard checks. Structure is the framework’s concern; the logic inside a method stays the
developer’s. That line is what lets the IR stay a faithful, lossless description of the model’s shape
without pretending to understand the business.
From that one artifact, the derived outputs follow. Documentation and schemas are generated from the IR today. API specification generation, contract testing that catches breaking changes before they ship, a schema registry, and visual exploration of the live domain are being built on the same foundation. The point is not the length of the list. The point is that each of these is a function of one source, so a change to the model is a change to one artifact, and everything derived from it moves in step.
A source of truth has to be true: the always-valid domain
Deriving from a model is only safe if the model is trustworthy. So correctness has to be a property of the model itself, in two ways most systems leave to luck.
The first is that invalid state should be impossible to construct. In most frameworks, validation is
something you opt into: you call validate() or is_valid(), and between those calls the object can
hold any state at all. Protean checks validity continuously. Domain objects are always valid, or they
do not exist. The guarantee is built from four layers, each catching a different class of error at the
earliest possible point:
- Field constraints catch types, ranges, and required-ness, declared on the field and enforced on every assignment.
- Value object invariants catch concept-level rules once, in one place: a
Moneythat cannot be negative, anEmailthat must be well-formed, valid wherever it is used. - Aggregate invariants catch business rules that span fields and child entities, and they run automatically on every mutation, recursively through the object graph, rolled back on failure.
- Handler and service guards catch the contextual rules that depend on who is asking and what else is true: authorization, timing, cross-aggregate constraints.
You write no validate() calls, and there is no window between method calls in which an object can be
invalid. When a coordinated change would be invalid step by step but valid as a whole, atomic_change
suspends the per-step checks and validates once at the end. The result is that a named method, a
direct field assignment, and a command handler are all equally safe, because the aggregate refuses any
change that would violate its rules.
The second is that the structure itself should be checked before the first request runs. At
domain.init(), Protean resolves every reference, checks association integrity and event-sourcing
constraints, and warns about unhandled commands, missing apply-handlers, and published events with no
external broker. A broken domain fails at startup, well before 3am in production. A model that is
always valid at runtime and proven sound at startup is a model you can derive from without fear,
because what you are deriving from is known to be correct. Under this view, constraints stop reading as
friction. They are the thing that makes everything built on top of the model trustworthy.
Complexity you grow into
Ambitious systems rarely arrive fully formed, and the shape you can see on day one is not the shape the system ends up with. Most tools make you pay for that uncertainty in rewrites: the plain model becomes CQRS and you rebuild, CQRS grows into event sourcing and you rebuild again, because each tool assumed the earlier shape.
Protean is designed so the model you wrote on the first day is still the model at scale. You start with plain domain-driven design: aggregates, application services, repositories, and a clean model with persistence. When one aggregate needs separate read and write paths, you add commands, handlers, and projections for that aggregate alone, and the others stay as they are. When one aggregate needs a full history for audit or temporal queries, you switch it to event sourcing without touching the rest. The three approaches coexist in one codebase, one domain, and one test suite, chosen per aggregate.
Technology follows the same principle. You start entirely in memory, with no database, broker, or event store to install, and prove the model works. You choose the production stack at the last responsible moment, and because that choice is configuration, switching later stays cheap. Software that grows by addition keeps its core intact long enough for that core to become the durable source of truth the rest is derived from. A model thrown away every eighteen months was only ever a draft.
Infrastructure is configuration
For the model to stay the single source of truth, it has to stay clean, which means it cannot know about the machinery underneath it. In Protean, the domain model knows nothing about databases, message brokers, or caches, and it cannot be made to, because the framework enforces the ports-and-adapters boundary instead of leaving it to discipline.
Which database, broker, or event store you use is a setting in domain.toml. The same field
declarations work identically whether they map to a PostgreSQL column or an Elasticsearch field. Moving
one aggregate from one store to another does not touch your business logic, your tests, or your rules.
The payoff runs deeper than convenience: the model tests run in memory in milliseconds, CI needs no
services for core logic, and the model that is the source of truth never gets entangled with the
infrastructure it happens to run on.
Why this matters more now than a year ago
For most of its life, this was a good idea carrying a real cost. Deriving everything requires a framework to understand everything, which means a lot of structure up front, more than a team in a hurry wanted to pay. Then machines started writing the code, and the arithmetic changed.
A domain model is a small, well-defined, rule-bound artifact, which makes it the best possible target for a machine. A general-purpose assistant can generate plausible Python, and plausible Python is exactly the problem: it will hand you an aggregate that quietly violates the single-writer rule, or a handler that mutates two aggregates in one transaction, and nothing catches it until production does. A model has rules a framework can check. An AI proposes a model, the framework validates it at startup, and the AI corrects what the framework rejects. Generation followed by verification is a far safer loop than generation alone.
The drift problem also gets sharply worse in a world of generated code. When a machine independently generates the model, the specification, the client, and the tests, those artifacts pull apart faster than a careful human would ever allow, because nothing ties them to a single truth. Deriving them all from one model is how a generated system stays coherent instead of fragmenting the moment it is built. The idea that was merely elegant a year ago has become the necessary one now.
The stance this requires
None of this works from a framework that tries to please everyone. Derivation demands opinions, and enforced ones: one aggregate changed per transaction, no infrastructure-specific code in the domain layer, validation in the model instead of the database, a single simple identity per aggregate, and a firm no to features that would fracture the model’s coherence. The same filter that decides what your code should contain decides what the framework itself should contain, and the answer is usually the smaller, more coherent option. A new capability often begins as a conversation about whether it belongs in the framework at all.
Tools with this property tend to come from a small number of hands, because coherence is hard to hold across a committee. SQLite, Lua, and TeX are the lineage. In each, the constraints are the product, and the single guiding view is why the thing holds together well enough to trust across decades. A framework broad enough to accommodate every preference could never understand your domain sharply enough to derive from it.
Where Protean stops
An honest description of a tool includes the systems it is wrong for, and naming them is what makes the rest believable.
Protean is for backend systems in Python where the hard part is the business rules and the states things move through, not moving large volumes of data at very low latency. If the difficulty in your system is that an order cannot ship before it is paid, or that a subscription in trial cannot be charged, it is built to help. If the difficulty is raw speed or crunching enormous datasets, it is not.
It is a poor fit for simple create-read-update-delete apps with little logic, for systems whose main challenge is latency or throughput, for analytics and data-processing pipelines, for systems modeled around database tables first, for retrofitting onto an existing app’s database models, for real-time collaborative editing that merges concurrent changes, and for active-active global deployments with strong consistency everywhere. For each of those, a different tool is the right one.
Two more honest notes. Protean is pre-1.0 and Apache-2.0 licensed, and its public surface changes only under a written compatibility contract. And leaving is bounded: the model is plain Python you own, with no framework base classes to inherit and no generated code you cannot read, so if you move off Protean later, the business logic is the part you keep. What you would replace is the wiring around it.
What endures
The code is the smaller half of this. What lasts is the way of thinking: model the domain well, make the model correct, derive everything derivable, and keep the model the one thing maintained by hand. Eric Evans’s book outlived every domain-driven framework of its time, because an idea travels further than any tool that instantiates it.
This is the idea, and a framework can be one honest instantiation of it. The test is concrete. The next time you add a field to an Order, you touch one place or you touch nine, and the difference is whether anything held the model as the single source of truth. The whiteboard you sketch the model on and the system that runs in production should be the same thing. Everything else flows from it.