This article is part 2 of a 7-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
Watch 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
Consider 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
In category theory, a morphism is a relationship from object to object . The key insight: morphisms have more structure than objects.
Properties of Morphisms
Morphisms can be:
| Property | Definition | Architectural Meaning |
|---|---|---|
| Monomorphism1 | Left-cancellable: | Injective transformation, no information loss |
| Epimorphism | Right-cancellable: | Surjective, covers entire target |
| Isomorphism | Has two-sided inverse | Lossless, reversible transformation |
| Endomorphism2 | Self-transformation (state machine) | |
| Automorphism | Isomorphic endomorphism | Symmetry operation |
Architectural Translation
# Monomorphism: User ID → User (no two IDs map to same user)
GET /users/{id} → User
# Epimorphism: Every valid response is reachable
POST /users → User # Can create any valid user state
# Isomorphism: Lossless encoding
JSON ↔ Protobuf # If done correctly
# Endomorphism: State transitions
PATCH /orders/{id}/status → Order # Same type in, same type out
# Automorphism: Reversible operations
PUT /users/{id}/toggle-active → User # Toggle again to reverse
Contract-First Design Is Morphism-First Design
When you write an OpenAPI specification before implementation, you’re doing morphism-first design:
openapi: 3.0.0
info:
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.
Hom-Sets: The Space of Possibilities
In category theory, 4 is the set of all morphisms from to . This is extraordinarily useful for architecture.
Analyzing Integration Points
For any two services and , ask: what is ?
Hom(OrderService, InventoryService) = {
checkAvailability: Order → Availability,
reserveItems: Order → Reservation,
releaseReservation: ReservationId → Unit,
decrementStock: Order → Unit
}
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
Tight 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 morphisms
interface TightlyCoupleddInventoryService {
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 morphisms
interface 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
Design principle: Minimize while maintaining required functionality.
This is the categorical formulation of interface segregation5 and loose coupling.
Applying the Principle
Before: One large API with many endpoints
Hom(Client, MonolithAPI) = { 50+ endpoints }
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
If and , then exists. This is the composition law.
Data Pipelines as Morphism Chains
RawEvent → Parse → Validate → Enrich → Transform → Store
A f g h i j
The entire pipeline is a single morphism:
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 morphism
const 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 composition
const pipeline = (raw: RawEvent): StoredEvent =>
store(transform(enrich(validate(parse(raw)))));
// Or more explicitly with pipe
const pipeline = pipe(parse, validate, enrich, transform, store);
Morphism Preservation Under Change
When systems evolve, the key question is: are the morphisms preserved?
API Versioning as Morphism Evolution
# v1
POST /v1/orders
Request: { items: [...], customer_id: string }
Response: { order_id: string, status: string }
# v2 - additive change (new optional field)
POST /v2/orders
Request: { 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 form of morphism factorization6:
Breaking Changes as Morphism Replacement
# Breaking change: different domain/codomain
POST /v2/orders
Request: { 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
Before any architecture review, enumerate the morphisms:
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
- 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
API Gateway
An API Gateway defines morphisms from external clients to internal services:
Hom(ExternalClient, Gateway) → Σ Hom(Gateway, BackendService)
The gateway is a morphism transformer—it takes external morphisms and maps them to internal ones.
EventBridge
EventBridge is a morphism composition engine:
Rule: EventPattern → Target
Each rule is a morphism. EventBridge composes them:
Source → [Rule1, Rule2, Rule3] → [Target1, Target2, Target3]
Step Functions
A 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
Think 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.
-
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. ↩
-
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 (or “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: . This is powerful for evolution: if v2 = extension ∘ v1, then v1 clients still work (they just ignore the extension). In many categories, every morphism factors uniquely 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). ↩