The thesis
The decisions that matter most in a backend system are the defaults a team accepts without thinking, and none of them looks like a decision. One is writing business logic inside an HTTP handler so it ships fast. Another is binding an ORM tightly to the schema, and a third is putting a model class on every entity. Each of those defaults feels harmless on day one. By month six they're the reason the team can't 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, and 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. My ContentFactory dealership platform is one deployed example of that shared core serving multiple buyer-facing channels. None of it is exotic, and all of it 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 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's 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 Model Context 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.
Hexagonal design sounds like extra ceremony, but 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 doesn't 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. The same property 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. The whole design is borrowed from disciplines that solved the same problem decades ago.
Functional core, imperative shell
Functional core, imperative shell is 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 pattern matters in production because of 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, and 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 person running the system wants to know "what does the system do when X," the answer is "look at the core function for X." There's one place to look, and no debate about whether the controller, the service, the repository, or the helper has the answer. The core has the answer, and everything else is plumbing.
Anti-patterns I see in client work
Three patterns show up repeatedly in audits, and they're usually load-bearing in a 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. A 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, and the boundary is a typed query layer that the operation calls. ORMs help PostgreSQL and harm everything else; don't pay the cost when you don't collect the benefit.
The third is god-classes for entities, like a User class with sixty methods or 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 instead of modifying 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.
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're unsure, default to small. If you're sure (you've 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. You pay upfront in discipline, and the return is compounding velocity.
If you're evaluating a backend team, the questions to ask 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've built won't compound. If they show you a typed operation, a thin adapter, a Pydantic schema, and a Hypothesis test, what they've built will.
