Morphisms All the Way Down: API Design as Arrow-First Thinking — Ibrahim Cesar

9 min read Original article ↗

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:

  1. “We need a User Service”
  2. “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 f:ABf: A \to B is a relationship from object AA to object BB. The key insight: morphisms have more structure than objects.

Properties of Morphisms

Morphisms can be:

PropertyDefinitionArchitectural Meaning
Monomorphism1Left-cancellable: fg=fhg=hf \circ g = f \circ h \Rightarrow g = hInjective transformation, no information loss
EpimorphismRight-cancellable: gf=hfg=hg \circ f = h \circ f \Rightarrow g = hSurjective, covers entire target
IsomorphismHas two-sided inverseLossless, reversible transformation
Endomorphism2f:AAf: A \to ASelf-transformation (state machine)
AutomorphismIsomorphic endomorphismSymmetry 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:

createOrder:CreateOrderRequestOrder\text{createOrder}: \text{CreateOrderRequest} \to \text{Order}

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, Hom(A,B)\text{Hom}(A, B)4 is the set of all morphisms from AA to BB. This is extraordinarily useful for architecture.

Analyzing Integration Points

For any two services AA and BB, ask: what is Hom(A,B)\text{Hom}(A, B)?

Hom(OrderService, InventoryService) = {

checkAvailability: Order → Availability,

reserveItems: Order → Reservation,

releaseReservation: ReservationId → Unit,

decrementStock: Order → Unit

}

This set tells you:

  1. All the ways A can interact with B
  2. The contracts that must be maintained
  3. The coupling surface between services

Coupling as Hom-Set Size

Tight coupling = large Hom(A,B)|\text{Hom}(A, B)|

Loose coupling = small Hom(A,B)|\text{Hom}(A, B)|

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 Hom(A,B)|\text{Hom}(A, B)| 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 f:ABf: A \to B and g:BCg: B \to C, then gf:ACg \circ f: A \to C 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: jihgf:AStoredEventj \circ i \circ h \circ g \circ f: A \to \text{StoredEvent}

Why This Matters

  1. Testability: Each morphism can be tested independently
  2. Replaceability: Swap any stage without affecting others
  3. Composability: Combine pipelines into larger pipelines
  4. 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:

  1. v1 requests are valid v2 requests (new field is optional)
  2. v1 response consumers can ignore new fields

This is a form of morphism factorization6: v2=extensionv1v2 = \text{extension} \circ v1

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

  1. What morphisms exist? List every API, event, message, RPC call
  2. What are their types? Domain → Codomain for each
  3. Which are essential? Remove any morphism—does the system still work?
  4. Which compose? Can you chain morphisms into pipelines?
  5. 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:

  1. Start with the morphisms (contracts, APIs, events)
  2. Let objects emerge from what the morphisms require
  3. Minimize coupling by minimizing hom-set sizes
  4. Ensure morphisms compose cleanly
  5. 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.

  1. 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.

  2. An endomorphism is a morphism from an object to itself: f:AAf: A \to A. 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.

  3. 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.

  4. A hom-set (or “hom-set,” short for “homomorphism set”) Hom(A,B)\text{Hom}(A, B) collects all morphisms from object AA to object BB 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.

  5. 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.

  6. Morphism factorization means expressing one morphism as the composition of others: f=ghf = g \circ h. 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).