The thesis
The decisions that matter most in a backend system are not the ones that look like decisions. They are the defaults the team accepts without thinking. The default to write business logic inside an HTTP handler so it ships fast. The default to bind an ORM tightly to the schema. The default to put a model class on every entity. Each of those defaults feels harmless on day one. By month six they are the reason the team cannot ship a second interface, swap a database, or add an agent surface without ripping things out and gluing them back together.
The shape that compounds is the opposite of those defaults. Business logic lives in a pure functional core. State lives at the imperative shell. The core never imports a transport. The boundaries are typed contracts, not class hierarchies. Data is decomposed into entities and components instead of bundled into model objects. Each interface (HTTP, CLI, MCP, agent surface, web UI) is a thin adapter over the same core. None of this is exotic. All of it is what determines whether the second feature ships in a week or a quarter.
Hexagonal: ports and adapters
The hexagonal pattern (Alistair Cockburn, 2005, sometimes called ports and adapters) is the architectural pattern that separates business logic from transport. The core (the hex) holds operations. Each operation has a typed input model and a typed output model. Each operation knows nothing about HTTP, nothing about the database, nothing about the user interface. It is a pure function from inputs to outputs, with side effects pushed to the boundary.
Around the hex sit adapters. An HTTP adapter parses an incoming request, validates it into the operation's input model, calls the operation, and serializes the output model into a response. A CLI adapter does the same thing with command-line arguments. An MCP adapter does the same thing with the MCP protocol. An agent adapter exposes the operation as a tool the agent can call. A test adapter calls the operation directly with crafted inputs. The adapters are thin and disposable; the core is thick and stable.
This sounds like extra ceremony. It is not. The first feature takes about as long to build hexagonally as it would inline. The second, third, and fourth features benefit from the hex because they get their interfaces for free. The team that built operations as inline HTTP handlers in feature one is rebuilding the same logic in CLI form in feature four; the team that built hexagonally is wiring a thirty-line adapter and moving on. The compounding is the entire reason to do it.
ECS: how data is shaped
ECS (Entity-Component-System) is borrowed from game engines but applies cleanly to backend systems. An entity is a typed identifier (a UUID with a type tag). Components are typed records attached to entities (a user has a profile component, an authentication component, a billing component, a preferences component). Systems are functions that operate on entities filtered by which components they have.
The reason ECS beats classical object-oriented modeling for backend work is composability. A new feature that adds behavior to users does not require modifying the User class. It adds a new component type, attaches it to the relevant entities, and writes a system function that operates on entities with that component. The existing components and systems are untouched. This is the same property that makes ECS the right choice for game engines: the part of the system that changes most often (behavior) is decoupled from the part that changes least (data shape).
The implementation in our stack is Pydantic V2 components stored in Convex tables. Each table holds entities of one type plus their components. Systems are pure functions that take a query (which entities, which components) and return new components (which entities should have what attached or detached). The Convex layer handles the persistence; the system functions handle the logic. The boundary between them is a typed query shape and a typed mutation shape. None of this is exotic. All of it is borrowed from disciplines that solved the same problem decades ago.
Functional core, imperative shell
Gary Bernhardt's pattern. The core is pure functions, each one a function from input to output with no side effects. The shell is the part of the system that does I/O: read from a database, call an external API, write a file. The shell calls the core with inputs, gets outputs back, and performs the side effects the outputs describe.
The reason this matters in production is testability. The core is trivially testable: hit it with property-based tests (Hypothesis on the Python side) that generate random valid inputs and check invariants on the outputs. The shell is testable through integration tests that exercise the I/O boundaries. The combined system has high coverage because the testing burden is distributed correctly: the part that has logic is tested by the test that proves logic, the part that has I/O is tested by the test that proves I/O.
The other reason it matters is that the core becomes the source of truth for behavior. When the operator wants to know "what does the system do when X," the answer is "look at the core function for X." There is one place to look. There is no debate about whether the controller, the service, the repository, or the helper has the answer. The core has the answer. Everything else is plumbing.
Anti-patterns I see in client work
Three patterns show up repeatedly in audits and they are usually load-bearing in the team's productivity collapse.
The first is fat HTTP handlers. The team treats the HTTP route function as the place where logic lives. Authentication, validation, business logic, persistence, and serialization all collapse into one 400-line function per route. The second interface is impossible because the logic is glued to HTTP. The fix is hexagonal extraction: pull each operation out of its handler, give it a typed input/output, and rewrite the handler as a fifteen-line adapter over the operation.
The second is ORM-as-architecture. The team uses an ORM and treats the ORM model as the source of truth for data shape, with business logic methods on the model. Every query gets entangled with the model, every refactor breaks N+1 query patterns, every new database becomes a rewrite. The fix is Pydantic-as-IR: Pydantic models hold the shape, Convex (or any backend) holds the persistence, the boundary is a typed query layer that the operation calls. ORMs help PostgreSQL and harm everything else; do not pay the cost when you do not collect the benefit.
The third is god-classes for entities. A User class with sixty methods. A Document class with eighty. The class becomes the bottleneck for every feature that touches the entity. The fix is ECS decomposition: kill the User class, replace it with a User entity (a typed ID) and components (profile, auth, billing, preferences) and system functions that operate on whichever components are relevant. A new feature adds a component; it does not modify the god-class.
When this is too much
The hexagonal-ECS-functional-core pattern earns its keep when the system has multiple interfaces (or will soon), when the team is more than three engineers, and when the system is intended to live for more than a year. Below those thresholds, the ceremony costs more than it returns. A two-person team building a single-interface MVP that may not survive contact with users should write fat HTTP handlers and ship; the architecture conversation is for after product-market-fit.
The honest framing is that architectural discipline is investment, not insurance. It pays back when the system grows; it costs without paying back when the system stays small. The judgment call is whether the system is going to grow. If you are unsure, default to small. If you are sure (you have shipped this size of system before, you have a roadmap, you have funded growth), default to disciplined.
What this gets you
A system designed this way takes longer to ship the first feature and dramatically less time to ship every subsequent feature. The team can swap databases, add interfaces, change models, or pivot product direction without rewriting the parts of the system that are stable. The cost is upfront discipline. The return is compounding velocity.
If you are evaluating a backend team, the questions are about the boundary between core and shell, the contract shape between operations and adapters, the data layer abstraction, and the test discipline. If they show you a controller class with business logic in it, what they have built will not compound. If they show you a typed operation, a thin adapter, a Pydantic schema, and a Hypothesis test, what they have built will.
