Skip to main content

en ~ 15 min read ~

Adjunctions: The Formal Structure of Trade-offs

Share this post

This article is part 11 of a 11-part series: Categorical Solutions Architecture

See the full series navigation at the end of this article.

“An adjunction is a pair of functors that are ‘almost inverses’ of each other, in a precise sense.”

You’ve felt this tension: more flexibility means less governance; more governance means less flexibility. More consistency means less availability. These feel like problems to solve, but they’re not—they’re mathematical dualities. Adjunctions formalize this structure, showing why certain trade-offs are necessary and how to navigate them optimally.

An adjunction between categories C\mathcal{C} and D\mathcal{D} consists of:1

  • A left adjoint functor F:CDF: \mathcal{C} \to \mathcal{D}
  • A right adjoint functor G:DCG: \mathcal{D} \to \mathcal{C}
  • A natural bijection:
HomD(F(C),D)HomC(C,G(D))\text{Hom}_{\mathcal{D}}(F(C), D) \cong \text{Hom}_{\mathcal{C}}(C, G(D))

We write FGF \dashv G (“F is left adjoint to G”).

CDunit ηηGFcounit εFGε
Adjunction F ⊣ G: unit η and counit ε
  • FF is “free”—it constructs with minimal constraints
  • GG is “forgetful”—it observes while losing structure
  • The bijection says: ways to use F’s output = ways to map into G’s output

The Canonical Example: Free and Forgetful

Section titled The Canonical Example: Free and Forgetful
Sets ⟵(U)---(F)⟶ Monoids
G F
  • FF: Free monoid (generates strings from a set)
  • GG: Forgetful functor (just sees the underlying set)

The adjunction says:

Monoid homomorphisms F(S)MFunctions SU(M)\text{Monoid homomorphisms } F(S) \to M \cong \text{Functions } S \to U(M)

In words: Giving a monoid homomorphism from the free monoid is the same as choosing where generators go.2

// The set: a type
type Generators = 'a' | 'b' | 'c';
// Free monoid: words over an arbitrary set S
type FreeMonoid<S> = S[];
// A target monoid
interface Monoid<M> {
empty: M;
combine: (a: M, b: M) => M;
}
// Right adjoint: forgetful, just sees the carrier
type Forget<M> = M;
// The adjunction: homomorphisms from Free ≅ functions to carrier
const adjunction = <M>(
monoid: Monoid<M>,
assignment: (g: Generators) => M // Function S → U(M)
): ((free: FreeMonoid<Generators>) => M) => { // Homomorphism F(S) → M
return (free: FreeMonoid<Generators>) =>
free.reduce((acc, g) => monoid.combine(acc, assignment(g)), monoid.empty);
};

Many architectural patterns have adjoint structure:3

Flexible System ⟵(Make Flexible)---(Observe)⟶ Constrained System

One warning before the table, because it’s easy to get wrong: the systems below are not adjoints. Functors are adjoints; systems are just objects. Read the columns as the two sides of the correspondence—shorthand for where each functor lands. And watch which side is which: in Sets ↔ Monoids, the free functor lands on the more-structured side (monoids), while the forgetful functor lands on the less-structured side (sets). Architectural “flexibility” usually names the less-structured side—the forgetful functor’s home turf, not the free functor’s.

Less-structured side (forgetful functor lands here)More-structured side (free functor lands here)
SetsMonoids
Schema-less (NoSQL)Schema-enforced (RDBMS)
Dynamic typingStatic typing
Event-drivenRequest-response
MicroservicesMonolith

Read the central formula again, carefully, because it’s easy to get backwards:

Maps out of F(C)    Maps into G(D)\text{Maps out of } F(C) \;\cong\; \text{Maps into } G(D)

This is a statement of equivalence, not scarcity. The bijection says the two descriptions are interchangeable: every way of using the free construction corresponds to exactly one way of mapping into the forgetful view, and the correspondence is natural—you can translate back and forth for free, as often as you like.

So where does the trade-off live? In the unit and counit, which are not invertible. Going from CC into G(F(C))G(F(C)), the free construction adds structure you never specified. Going from F(G(D))F(G(D)) back to DD, the forgetful view has already discarded structure you had. The adjunction organizes free translation between two perspectives; what each direction must add or forget is where the cost lives.


It’s tempting to say the CAP theorem is an adjunction in disguise. Resist the temptation—it isn’t one, and seeing why sharpens both ideas.

CAP is an impossibility theorem: under a network partition, no system can offer both strong consistency and full availability.4 An adjunction is the opposite kind of statement—a correspondence. If a hom-set bijection related consistent-side specifications to available-side specifications, it would say the two specs carry equivalent information. That’s the opposite of impossibility. CAP itself is not the bijection, and no amount of squinting makes it one.

What can be adjunction-flavored is the pair of translations between the two worlds. Take categories of systems annotated with their consistency guarantees, and a pair of functors between them:

Available Systems ⟵(Weaken)---(Strengthen)⟶ Consistent Systems
G F
  • FF: Strengthen—add the coordination required to meet a consistency contract
  • GG: Weaken—forget guarantees, keep the behavior

If these form an adjunction, the correspondence reads:

Maps out of the strengthened systemMaps into the weakened view\text{Maps out of the strengthened system} \cong \text{Maps into the weakened view}

That’s a claim about translating specifications—not a way around CAP. The trade-off lives where it always does: strengthening adds coordination (and its latency) you never asked for; weakening discards guarantees you were relying on. And CAP remains standing outside the whole construction, as the hard limit on what any strengthening can deliver during a partition.

// Consistent system
interface ConsistentDB<T> {
read(): Promise<{ value: T; version: number }>;
write(value: T, expectedVersion: number): Promise<boolean>; // CAS
}
// Available system (weakened view)
interface AvailableDB<T> {
read(): Promise<T>; // May be stale
write(value: T): Promise<void>; // Always succeeds locally
}
// The adjunction-flavored claim: programming against ConsistentDB<T>
// corresponds to programming against AvailableDB<T> plus explicit
// conflict resolution—the part that Strengthen has to add
Note:

CAP Is Not an Adjunction

The CAP theorem says: under partition, you must choose C or A. That’s an impossibility result about systems. The adjunction-shaped structure here is the strengthen ⊣ weaken pair of translations—and naturality means you always have both descriptions available, freely interchangeable. Keep the two ideas separate.


Another fundamental adjunction:

High Performance ⟵(Scale Up)---(Reduce)⟶ Low Cost
F G

The correspondence:

Maps out of the scaled-up systemMaps into the reduced view\text{Maps out of the scaled-up system} \cong \text{Maps into the reduced view}

Same shape as before: this is translation, not scarcity. A design phrased against the scaled-up system can be re-read, exactly, as a design phrased against its reduced view. The trade-off sits in the unit and counit—scaling up provisions capacity you didn’t specify; reducing forgets headroom you had.

// High performance (provisioned)
const provisionedDynamoDB = {
readCapacityUnits: 10000,
writeCapacityUnits: 5000,
// Predictable latency, higher cost
};
// Low cost (on-demand)
const onDemandDynamoDB = {
billingMode: 'PAY_PER_REQUEST',
// Variable latency, pay per use
};
// Adjunction: provisioned capacity guarantees are equivalent to
// on-demand costs under specific load patterns

Every adjunction has two special natural transformations:

Unit: η:IdCGF\eta: \text{Id}_{\mathcal{C}} \Rightarrow G \circ F

Section titled Unit: η:IdC⇒G∘F\eta: \text{Id}_{\mathcal{C}} \Rightarrow G \circ Fη:IdC​⇒G∘F

“Embedding into the free construction”

// Unit: embed a set into its free monoid
const unit = <S>(s: S): FreeMonoid<S> => [s];
// Architecturally: embed simple request into flexible system
const embedRequest = (simpleReq: SimpleRequest): FlexibleRequest => ({
...simpleReq,
metadata: {},
options: defaults,
});

Counit: ϵ:FGIdD\epsilon: F \circ G \Rightarrow \text{Id}_{\mathcal{D}}

Section titled Counit: ϵ:F∘G⇒IdD\epsilon: F \circ G \Rightarrow \text{Id}_{\mathcal{D}}ϵ:F∘G⇒IdD​

“Evaluating the free construction”

// Counit: evaluate free monoid in target monoid
const counit = <M>(
monoid: Monoid<M>,
free: FreeMonoid<M>
): M =>
free.reduce((acc, m) => monoid.combine(acc, m), monoid.empty);
// Architecturally: evaluate flexible request in constrained system
const evaluateInConstrained = (
system: ConstrainedSystem,
flexReq: FlexibleRequest
): ConstrainedResponse =>
system.handle(restrictToSchema(flexReq));

The unit and counit satisfy:

(Gϵ)(ηG)=idG(G\epsilon) \circ (\eta G) = \text{id}_G (ϵF)(Fη)=idF(\epsilon F) \circ (F\eta) = \text{id}_F

Meaning: Not that round-tripping through both functors gets you back where you started—it doesn’t. The free monoid on U(M)U(M) is nothing like MM; if the round trips were identities, we’d have an equivalence of categories, not an adjunction. The triangle identities say something narrower: inserting the unit–counit zig-zag on the image of FF or GG is a no-op. FFGFFF \Rightarrow FGF \Rightarrow F is the identity, and so is GGFGGG \Rightarrow GFG \Rightarrow G.


Every adjunction FGF \dashv G generates a monad T=GFT = G \circ F on C\mathcal{C}.5

// The monad from Free ⊣ Forgetful
type FreeMonad<S> = FreeMonoid<S>; // G(F(S))
// unit: S → G(F(S))
const pure = <S>(s: S): FreeMonad<S> => [s];
// join: G(F(G(F(S)))) → G(F(S))
const flatten = <S>(nested: FreeMonad<FreeMonad<S>>): FreeMonad<S> =>
nested.flat();

This is why monads appear everywhere in programming—they arise from adjunctions, and adjunctions model trade-offs.


Pick left or right and commit:

// Commit to the flexible side
const flexibleChoice = 'microservices';
// Accept: more operational complexity, coordination costs
// Commit to the constrained side
const constrainedChoice = 'monolith';
// Accept: less flexibility, tighter coupling

Work on the easier side, transport results:6

// Problem: design complex consistent transactions
// Easier: design available operations + conflict resolution
// Work in available space
const operations = designAvailableOps();
// Transport to consistent space via adjunction
const transactions = composeWithConflictResolution(operations);

Strategy 3: Move Along the Adjunction

Section titled Strategy 3: Move Along the Adjunction

Use unit/counit to transition:

// Start in simple category
const simpleDesign: SimpleSystem = { /* ... */ };
// Apply unit: embed into flexible
const flexibleDesign = unit(simpleDesign);
// Modify in flexible space
const modified = enhance(flexibleDesign);
// Apply counit: evaluate back in constrained
const constrainedResult = counit(targetSystem, modified);

Lambda (Flexible) ⟵(Extract)---(Containerize)⟶ ECS (Controlled)
  • Left: Lambda—flexible scaling, pay-per-invocation
  • Right: ECS—controlled resources, predictable behavior

The correspondence, honestly stated: if Containerize ⊣ Extract holds, ways of using a containerized workload correspond to ways of mapping into its extracted, Lambda-shaped view—a bijection of hom-sets, natural in both arguments. It is not an object-level bijection between Lambda configurations and ECS task definitions; that would be an equivalence, and none exists (an ECS task exceeding Lambda’s limits has no Lambda counterpart).

DynamoDB (Flexible Schema) ⟵(Relax)---(Normalize)⟶ RDS (Rigid Schema)
  • Left: DynamoDB—schema flexibility, denormalized
  • Right: RDS—schema enforcement, normalized

The correspondence, honestly stated: if Normalize ⊣ Relax holds, ways of using the normalized schema correspond to ways of mapping access patterns into its relaxed view—hom-sets again, not a one-to-one pairing of schemas or queries.

SNS (Pub/Sub) ⟵(Broadcast)---(Buffer)⟶ SQS (Queue)
  • Left: SNS—immediate fanout, fire-and-forget
  • Right: SQS—buffered, guaranteed delivery

The correspondence, honestly stated: if Buffer ⊣ Broadcast holds, ways of consuming the buffered topic correspond to ways of mapping into the broadcast view of the queue—a hom-set correspondence, not a claim that every consumer setup has a subscription twin.


Signs you have an adjunction:

1. A Correspondence Between Ways of Mapping

Section titled 1. A Correspondence Between Ways of Mapping

“Every way of using one construction matches a way of mapping into the other”—a correspondence of hom-sets, not a one-to-one pairing of the systems themselves

GraphQL queries ↔ REST endpoint compositions
Event handlers ↔ Request handlers
Declarative configs ↔ Imperative scripts

“We can’t have both fully”

Flexibility ↔ Governance
Performance ↔ Cost
Simplicity ↔ Power
Latency ↔ Throughput

“There’s a natural way to go from A to B”—but be precise about what those translations are. A REST-to-GraphQL translation is the action of the functor FF itself, carrying objects across categories. It is not the unit: η\eta lives entirely inside C\mathcal{C}, comparing each object with its round-trip image G(F(C))G(F(C)), and ϵ\epsilon lives entirely inside D\mathcal{D}.

// The action of F: translate across categories
declare const restToGraphQL: (endpoint: RestEndpoint) => GraphQLResolver;
// The action of G: translate back
declare const graphqlToRest: (resolver: GraphQLResolver) => RestEndpoint;
// The unit stays INSIDE the REST category:
// η: endpoint → graphqlToRest(restToGraphQL(endpoint))
Tip:

The Adjunction Test

Careful with the tempting phrasing “natural embeddings both ways”—that describes a section–retraction pair or an equivalence, not an adjunction. The adjunction signature is asymmetric: two translations, a natural correspondence between maps out of one construction and maps into the other, and round trips (unit and counit) that are systematic but not invertible.


Adjunctions organize architectural trade-offs:

  1. Left adjoint (free): constructive—lands on the more-structured side, adding structure you didn’t specify
  2. Right adjoint (forgetful): observational—lands on the less-structured side, discarding structure you had
  3. Hom-set correspondence: maps out of the free construction match maps into the forgetful view—two interchangeable descriptions of the same information
  4. Unit/Counit: the non-invertible round trips—where the actual trade-off lives

When you feel tension between flexibility and constraint:

  • Ask whether a pair of translations gives it adjoint structure
  • Use the correspondence to work on the easier side
  • Remember the cost sits in the unit and counit, not in the bijection

Trade-offs aren’t problems—they’re structure. Navigate them, don’t fight them.


Next in the series: Monads: Cross-Cutting Concerns That Actually Compose — Where we learn how error handling, logging, and effects can be structured categorically.


  1. Adjoint functors were introduced by Daniel Kan in “Adjoint Functors” (Transactions of the American Mathematical Society, 1958), motivated by his work in homotopy theory, where the pattern kept recurring between constructions like product and function-space—what programmers know as currying. The definition can be given two equivalent ways: by the natural hom-set bijection used in this post, or by a unit and counit satisfying the triangle identities. The name is borrowed from linear algebra, where adjoint operators satisfy Tx,y=x,Ty\langle Tx, y \rangle = \langle x, T^{*}y \rangle—the hom-set bijection is the categorical echo of that inner-product identity, with Hom playing the role of the pairing.

  2. The free ⊣ forgetful pair is the paradigm adjunction. The free monoid on a set SS is the set of finite words over SS under concatenation, and its universal property is exactly the bijection in the text: any function SU(M)S \to U(M) extends to a unique homomorphism F(S)MF(S) \to M. The same pattern recurs across algebra—free groups, free rings, free vector spaces on a basis, free categories on a graph—each a left adjoint to the evident forgetful functor. This is why “free” is a technical term rather than a vibe: freely generated means no equations hold beyond those the structure itself forces.

  3. Saunders Mac Lane’s Categories for the Working Mathematician (Springer, 1971; second edition 1998) declares in its preface that “adjoint functors arise everywhere,” and the book is organized to prove the slogan—limits, colimits, monads, and Kan extensions are all developed as facets of adjunction. Mac Lane, who founded category theory with Samuel Eilenberg in the 1940s, treated the adjunction as the concept the subject had been converging toward all along. If one idea justifies category theory to a practitioner, this is the usual nominee—which is exactly why it deserves to be quoted accurately rather than stretched.

  4. The CAP theorem began as Eric Brewer’s conjecture in his PODC 2000 keynote and was formalized and proved by Seth Gilbert and Nancy Lynch in “Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services” (ACM SIGACT News, 2002). Gilbert and Lynch’s result is an impossibility theorem: in an asynchronous network subject to partitions, no read/write object can guarantee both linearizable consistency and a response to every request. That logical shape—“nothing with both properties exists”—is precisely what an adjunction never asserts; an adjunction asserts that two hom-sets are in natural bijection. Part 9 of this series used the same theorem to classify pullback strategies; here it marks the boundary that no strengthen ⊣ weaken translation can cross.

  5. That every adjunction induces a monad was known early; the converse—that every monad arises from some adjunction—was proved independently in 1965 by Heinrich Kleisli (“Every Standard Construction Is Induced by a Pair of Adjoint Functors,” Proceedings of the American Mathematical Society) and by Samuel Eilenberg and John C. Moore (“Adjoint Functors and Triples,” Illinois Journal of Mathematics). The two constructions bracket the possibilities: the Kleisli category is the smallest adjunction generating a given monad, the Eilenberg–Moore category of algebras the largest. At the time monads were called “standard constructions” or “triples”; the modern name stuck through Mac Lane. When Eugenio Moggi later connected monads to computational effects in “Notions of Computation and Monads” (Information and Computation, 1991), this 1965 machinery became the mathematical backbone of effectful functional programming.

  6. “Work on the easier side and transport the results” is backed by a real theorem: right adjoints preserve limits (RAPL), and dually, left adjoints preserve colimits—see Mac Lane, Categories for the Working Mathematician, Chapter V. Concretely: the underlying set of a product of monoids is the product of the underlying sets (the forgetful functor, a right adjoint, preserves products), and the free monoid on a disjoint union is the coproduct of the free monoids (the free functor, a left adjoint, preserves coproducts). Freyd’s adjoint functor theorem gives a partial converse: a limit-preserving functor satisfying a solution-set condition is a right adjoint. For an architect, this is the payoff of identifying adjoint structure—preservation guarantees you get for free instead of verifying case by case.

Share this post

Comments

Favorite Books

Links are Amazon affiliate links.