en ~ 11 min read ~
Morphisms All the Way Down: API Design as Arrow-First Thinking
Share this post
This article is part 2 of a 8-part series: Categorical Solutions Architecture
See the full series navigation at the end of this article.
“In mathematics you don’t understand things. You just get used to them.”
— John von Neumann
Traditional architecture starts with boxes: services, databases, components. Categorical architecture starts with arrows: the relationships, contracts, and transformations between things. This shift from entity-first to relationship-first thinking transforms how we design systems. The arrows are the architecture.
The Entity Trap
Section titled The Entity TrapWatch any architecture review. The discussion inevitably centers on:
- “What does this service do?”
- “What data does this database store?”
- “What’s the responsibility of this component?”
These are the wrong questions. They focus on objects when we should focus on morphisms.
A Thought Experiment
Section titled A Thought ExperimentConsider two architecturally identical questions:
- “We need a User Service”
- “We need these operations on user data: create, read, update, delete, authenticate, authorize”
The first answer gives you a box. The second gives you arrows. The arrows are what matter.
A “User Service” that exposes no operations is architecturally meaningless. But a set of well-defined operations? That’s a contract. That’s something you can implement, test, version, and evolve.
Morphisms as First-Class Citizens
Section titled Morphisms as First-Class CitizensIn category theory, a morphism is a relationship from object to object . The key insight: morphisms have more structure than objects.
Properties of Morphisms
Section titled Properties of MorphismsMorphisms can be:
| Property | Definition | Architectural Meaning |
|---|---|---|
| Monomorphism1 | Left-cancellable: | Injective transformation, no information loss |
| Epimorphism | Right-cancellable: | Surjective, covers entire target (in )1 |
| Isomorphism | Has two-sided inverse | Lossless, reversible transformation |
| Endomorphism2 | Self-transformation (state machine) | |
| Automorphism | Isomorphic endomorphism | Symmetry operation |
Architectural Translation
Section titled Architectural Translation# Monomorphism: User ID → User (no two IDs map to same user)GET /users/{id} → User
# Epimorphism: Every valid response is reachablePOST /users → User # Can create any valid user state
# Isomorphism: Lossless encodingJSON ↔ Protobuf # If done correctly
# Endomorphism: State transitionsPATCH /orders/{id}/status → Order # Same type in, same type out
# Automorphism: Reversible operationsPUT /users/{id}/toggle-active → User # Toggle again to reverseContract-First Design Is Morphism-First Design
Section titled Contract-First Design Is Morphism-First DesignWhen you write an OpenAPI specification before implementation, you’re doing morphism-first design:
openapi: 3.0.0info: title: Order Service version: 1.0.0
paths: /orders: post: summary: Create order requestBody: content: application/json: schema: $ref: '#/components/schemas/CreateOrderRequest' responses: '201': content: application/json: schema: $ref: '#/components/schemas/Order'This specification defines a morphism:
The implementation is categorically invisible. Whether you use Node.js, Go, or COBOL3—whether you store in PostgreSQL, DynamoDB, or carrier pigeons—the morphism is the same.
The Contract IS the Architecture
Implementation details are morphisms in a different category (the “implementation category”). The architecture lives in the “interface category.” These categories are connected by a functor (implementation), but the interface category is what consumers see.
Hom-Sets: The Space of Possibilities
Section titled Hom-Sets: The Space of PossibilitiesIn category theory, 4 is the set of all morphisms from to . This is extraordinarily useful for architecture.
Analyzing Integration Points
Section titled Analyzing Integration PointsFor any two services and , ask: what operations does invoke on ?
Coupling(OrderService, InventoryService) = { checkAvailability: Order → Availability, reserveItems: Order → Reservation, releaseReservation: ReservationId → Unit, decrementStock: Order → Unit}One precision: each operation above is a morphism between data types, so this set isn’t literally —every element of a hom-set must go from to , and these don’t. With services as objects, each operation is instead one arrow : four parallel arrows, with the payload typing living in the category of data types. That hom-set is the coupling surface.
This set tells you:
- All the ways A can interact with B
- The contracts that must be maintained
- The coupling surface between services
Coupling as Hom-Set Size
Section titled Coupling as Hom-Set SizeTight coupling = large
Loose coupling = small
If Service A has 47 different ways to call Service B, they’re tightly coupled. If there’s exactly one well-defined interface, they’re loosely coupled.
// Tight coupling: many morphismsinterface TightlyCoupledInventoryService { checkStock(sku: string): number; checkStockBatch(skus: string[]): Map<string, number>; reserveStock(sku: string, qty: number): boolean; reserveStockWithExpiry(sku: string, qty: number, ttl: number): boolean; releaseStock(reservationId: string): void; releaseStockPartial(reservationId: string, qty: number): void; getReservation(id: string): Reservation; listReservations(sku: string): Reservation[]; // ... 15 more methods}
// Loose coupling: minimal morphismsinterface LooselyCoupledInventoryService { checkAvailability(items: Item[]): Availability; reserve(items: Item[], ttl: Duration): Reservation; release(reservation: Reservation): void;}The second interface is easier to implement, test, mock, and evolve.
The Principle of Minimal Morphisms
Section titled The Principle of Minimal MorphismsDesign principle: Minimize while maintaining required functionality.
This is the categorical formulation of interface segregation5 and loose coupling.
Applying the Principle
Section titled Applying the PrincipleBefore: One large API with many endpoints
Hom(Client, MonolithAPI) = { 50+ endpoints }(Services as objects again: each endpoint is one arrow .)
After: Multiple focused APIs
Hom(Client, UserAPI) = { 5 endpoints }Hom(Client, OrderAPI) = { 8 endpoints }Hom(Client, PaymentAPI) = { 4 endpoints }Total endpoints might be similar, but each hom-set is smaller and more coherent.
Composing Morphisms: The Power of Pipelines
Section titled Composing Morphisms: The Power of PipelinesIf and , then exists. This is the composition law.
Data Pipelines as Morphism Chains
Section titled Data Pipelines as Morphism ChainsRawEvent → Parse → Validate → Enrich → Transform → Store A f g h i jThe entire pipeline is a single morphism:
Why This Matters
Section titled Why This Matters- Testability: Each morphism can be tested independently
- Replaceability: Swap any stage without affecting others
- Composability: Combine pipelines into larger pipelines
- Reasoning: The whole is the composition of its parts
// Each stage is a morphismconst parse = (raw: RawEvent): ParsedEvent => { /* ... */ };const validate = (parsed: ParsedEvent): ValidEvent => { /* ... */ };const enrich = (valid: ValidEvent): EnrichedEvent => { /* ... */ };const transform = (enriched: EnrichedEvent): TransformedEvent => { /* ... */ };const store = (transformed: TransformedEvent): StoredEvent => { /* ... */ };
// The pipeline is their compositionconst pipeline = (raw: RawEvent): StoredEvent => store(transform(enrich(validate(parse(raw)))));
// Or more explicitly with pipeconst composedPipeline = pipe(parse, validate, enrich, transform, store);Morphism Preservation Under Change
Section titled Morphism Preservation Under ChangeWhen systems evolve, the key question is: are the morphisms preserved?
API Versioning as Morphism Evolution
Section titled API Versioning as Morphism Evolution# v1POST /v1/ordersRequest: { items: [...], customer_id: string }Response: { order_id: string, status: string }
# v2 - additive change (new optional field)POST /v2/ordersRequest: { items: [...], customer_id: string, priority?: string }Response: { order_id: string, status: string, estimated_delivery?: date }The v2 morphism is backward compatible because:
- v1 requests are valid v2 requests (new field is optional)
- v1 response consumers can ignore new fields
This is a compatibility square6: with the inclusion and extension , restricting v2 to old requests factors through v1—. Project away the new response fields with and you recover v1 exactly: .
Breaking Changes as Morphism Replacement
Section titled Breaking Changes as Morphism Replacement# Breaking change: different domain/codomainPOST /v2/ordersRequest: { line_items: [...], customer: { id: string, segment: string } }Response: { id: uuid, state: OrderState, timeline: [...] }This isn’t an evolution of the same morphism—it’s a different morphism. The categorical view makes this clear: you’ve changed both domain and codomain.
The Morphism Audit
Section titled The Morphism AuditBefore any architecture review, enumerate the morphisms:
Questions to Ask
Section titled Questions to Ask- What morphisms exist? List every API, event, message, RPC call
- What are their types? Domain → Codomain for each
- Which are essential? Remove any morphism—does the system still work?
- Which compose? Can you chain morphisms into pipelines?
- What’s missing? Are there implied relationships not made explicit?
Red Flags
Section titled Red Flags- Morphisms with unclear types: “This API returns… whatever the backend sends”
- Non-composable morphisms: “You have to call A, then call B with the result, then…”
- Duplicate morphisms: Three different ways to get user data
- Circular morphisms: A calls B calls C calls A
AWS Through the Morphism Lens
Section titled AWS Through the Morphism LensAPI Gateway
Section titled API GatewayIn the services-as-objects picture, an API Gateway maps external routes—arrows into the gateway—to internal calls:
Hom(ExternalClient, Gateway) → Σ Hom(Gateway, BackendService)The gateway is a morphism transformer—it takes external morphisms and maps them to internal ones.
EventBridge
Section titled EventBridgeEventBridge is a routing engine:
Rule: Event → TargetInput # restricted to events matching the patternEach rule is a morphism. But fan-out is not composition—rules sharing the same source can’t compose with each other. It’s tupling, the universal property of the product:
⟨Rule1, Rule2, Rule3⟩: Source → Target1 × Target2 × Target3Step Functions
Section titled Step FunctionsA state machine is a collection of morphisms between states:
Hom(State1, State2) = { transition1, transition2 }Hom(State2, State3) = { transition3 }...The workflow is the composition of these state transitions.
The Takeaway
Section titled The TakeawayThink in arrows, not boxes.
When designing systems:
- Start with the morphisms (contracts, APIs, events)
- Let objects emerge from what the morphisms require
- Minimize coupling by minimizing hom-set sizes
- Ensure morphisms compose cleanly
- Evolve morphisms carefully—breaking changes break composition
The architecture is the collection of morphisms. Everything else is implementation detail.
Next in the series: The Yoneda Perspective: Systems Defined by Their Interfaces — Where we discover that knowing all the morphisms into and out of an object tells you everything about it.
Footnotes
Section titled Footnotes-
Monomorphism, epimorphism, and isomorphism are the categorical generalizations of injective (one-to-one), surjective (onto), and bijective (one-to-one and onto) functions. A monomorphism preserves distinctness: different inputs give different outputs. An epimorphism covers the target: every possible output is reachable. An isomorphism does both, meaning you can go back and forth without losing information. These concepts generalize beyond functions to any category—with one caution: in general categories epimorphisms need not be surjective (the ring inclusion is an epi); the “covers the target” reading is specific to . ↩ ↩2
-
An endomorphism is a morphism from an object to itself: . Think state transitions, where you start with a state and end with a (possibly different) state of the same type. An automorphism is an endomorphism that’s also an isomorphism—a reversible self-transformation. Rotating a square 90° is an automorphism; the square is still a square and you can rotate back. ↩
-
COBOL stands for “Category-Oriented Business Object Language” — just kidding. It’s actually Common Business-Oriented Language, created in 1959 and still running an estimated 95% of ATM transactions and 80% of in-person transactions globally. The joke here is that from a categorical perspective, it genuinely doesn’t matter if your morphism is implemented in a hip new language or a 65-year-old one. The interface is what counts. ↩
-
A hom-set (short for “homomorphism set”) collects all morphisms from object to object in a category. It’s pronounced “hom A B” or “hom-set from A to B.” In programming terms, it’s like asking “what are all the functions with signature
A → B?” The size of a hom-set measures how many ways two things can relate—a key metric for coupling. ↩ -
Interface segregation is the “I” in SOLID principles, stating that clients shouldn’t depend on interfaces they don’t use. The categorical view: minimize the hom-set size that each client sees. Instead of one fat interface with 20 methods, provide 4 focused interfaces with 5 methods each. Each client depends only on the morphisms it actually uses. ↩
-
Morphism factorization means expressing one morphism as the composition of others: . For API evolution the honest statement is a compatibility square: with and , backward compatibility means —v2 restricted to old requests factors through v1, so v1 clients still work (they just ignore the extension). In many categories, every morphism factors essentially uniquely (uniquely up to isomorphism) through an epi followed by a mono—the “image factorization.” This corresponds to: first collapse to what’s essential (epi), then embed in the target (mono). ↩