Skip to main content

en ~ 13 min read ~

Limits and Colimits: The General Theory of 'Best Fit'

Share this post

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

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

“Limits and colimits are the most important constructions in category theory. Everything else is a special case.”

We’ve seen products, coproducts, pullbacks, and pushouts. They seem different, but they’re all instances of a single concept: limits and colimits. Understanding this generalization reveals the deep structure underlying all universal constructions—and gives you a unified framework for architectural design.

The Pattern Behind the Patterns

Section titled The Pattern Behind the Patterns

Look at what we’ve covered:

ConstructionWhat It DoesUniversal Property
Product A × BCombines A and BBest way to access both
Coproduct A + BOffers choice of A or BBest way to accept either
PullbackCombines A and B consistently over CBest consistent combination
PushoutMerges A and B identifying along CBest identified merge
Terminal object 1The “single point”Unique morphism from anything
Initial object 0The “empty” objectUnique morphism to anything

All of these share a pattern: they’re the “best” solution to a specific problem, where “best” means every other solution factors through them.


Limits: The General Construction

Section titled Limits: The General Construction

A limit is the most general way to “combine” objects subject to constraints.1

Given a diagram D:JCD: J \to \mathcal{C} (a shape JJ mapped into your category), the limit limD\lim D is:

  1. An object with morphisms to each object in the diagram
  2. Such that all triangles commute (constraints are satisfied)
  3. With the universal property: any other such object factors uniquely through it
lim D
↙ ↓ ↘
D(1) D(2) D(3)
↘ ↓ ↙
D(4)

The limit sits “above” the entire diagram, projecting to each piece while respecting the diagram’s internal structure.


Terminal Object (Empty Diagram)

Section titled Terminal Object (Empty Diagram)

When JJ is empty, the limit is the terminal object 11:

// Terminal object: unit type
type Unit = void;
// Unique morphism from any type
const toUnit = <A>(a: A): Unit => undefined;

Architectural meaning: The “trivial” service that everything can ignore.

When JJ has two objects with no morphisms between them:2

J: • • (two disconnected points)
lim = A × B with projections π₁, π₂

Architectural meaning: Gateway combining independent services.

When JJ has two objects with two parallel morphisms:

J: • ⇉ •
lim = Eq(f, g) = { x | f(x) = g(x) }

Architectural meaning: Consistency check between two computations.

// Equalizer: inputs where two functions agree
declare const deepEqual: (a: unknown, b: unknown) => boolean;
const equalizer = <A, B>(
f: (a: A) => B,
g: (a: A) => B
): ((a: A) => A | null) => {
return (a: A) => deepEqual(f(a), g(a)) ? a : null;
};

When JJ has the shape \bullet \to \bullet \leftarrow \bullet:

J: A → C ← B
lim = A ×_C B (pullback)

Architectural meaning: Consistent join over shared reference.


Colimits: The Dual Construction

Section titled Colimits: The Dual Construction

Colimits are dual to limits—they’re the “best” way to merge or combine by identification.

Given a diagram D:JCD: J \to \mathcal{C}, the colimit colim D\text{colim } D is:

  1. An object with morphisms FROM each object in the diagram
  2. Such that all triangles commute
  3. Universal: for any other cocone with apex QQ, there is a unique morphism colim DQ\text{colim } D \to Q commuting with the cocone legs
D(4)
↗ ↑ ↖
D(1) D(2) D(3)
↘ ↓ ↙
colim D

The colimit sits “below” the diagram, receiving from each piece.


Initial Object (Empty Diagram)

Section titled Initial Object (Empty Diagram)

When JJ is empty, the colimit is the initial object 00:

// Initial object: never/void type
type Never = never;
// Unique morphism to any type
const fromNever = <A>(n: Never): A => n; // absurd

Architectural meaning: The “impossible” service that can produce anything (because it never runs).

When JJ has two disconnected objects:

J: • •
colim = A + B with injections ι₁, ι₂

Architectural meaning: Event router accepting from multiple sources.

When JJ has parallel morphisms:

J: • ⇉ •
colim = Coeq(f, g) = B / (f(a) ~ g(a))

Architectural meaning: Merging where two computations are treated as equivalent.3

// Coequalizer: quotient by equivalence
type EquivalenceClass<B> = { representative: B };
// Identify f(a) with g(a) for all a; map each b to its class
declare const coequalizer: <A, B>(
f: (a: A) => B,
g: (a: A) => B
) => (b: B) => EquivalenceClass<B>;

When JJ has the shape \bullet \leftarrow \bullet \to \bullet:

J: A ← C → B
colim = A +_C B (pushout)

Architectural meaning: Merge identifying common structure.


LimitColimitRelationship
Terminal object 1Initial object 0Unique in/out
Product A × BCoproduct A + BCombine/Choose
EqualizerCoequalizerAgree/Identify
PullbackPushoutConsistent combine/Merge
LimitColimitProject into/Receive from

Complete and Cocomplete Categories

Section titled Complete and Cocomplete Categories

A category is complete if it has all small limits. It’s cocomplete if it has all small colimits.4

Sets Are Complete and Cocomplete

Section titled Sets Are Complete and Cocomplete
// All small limits exist in Set (TypeScript types)
// Terminal: Unit
type Unit = void;
// Products: tuples
type Product<A, B> = [A, B];
// Equalizers: subsets
type Equalizer<A, B> = A & { _eq: true }; // Conceptually
// All small colimits exist too
// Initial: Never
type Never = never;
// Coproducts: unions
type Coproduct<A, B> = A | B;
// Coequalizers: quotients (harder to express)

Is your service architecture complete?

Has terminal object? Can every service “do nothing” (health check)?

Has products? Can you aggregate any two services?

Has equalizers? Can you check consistency between any two computations?

If any of these fail, some constructions are impossible in your architecture.


An API Gateway over multiple Lambda functions is a limit:

API Gateway
↙ ↓ ↘
Lambda1 Lambda2 Lambda3

It’s the universal way to access all three consistently.

DynamoDB Transactions: Joint Consistency

Section titled DynamoDB Transactions: Joint Consistency

TransactWriteItems is closer to a product with an atomicity guarantee:5

// Multiple items committed together (all succeed or all fail)
await client.send(new TransactWriteItemsCommand({
TransactItems: [
{ Put: { TableName: 'orders', Item: order } },
{ Update: {
TableName: 'inventory',
Key: { sku: { S: sku } },
UpdateExpression: 'SET qty = qty - :dec',
ExpressionAttributeValues: { ':dec': { N: '1' } },
} },
],
}));
// Not a literal pullback—a product whose legs commit jointly.
// "Joint consistency" is the analogy, not the construction.

The limit over discrete data points is their product—the joint record of all of them:

(datapoint1, datapoint2, datapoint3, ...)
↙ ↓ ↘
datapoint1 datapoint2 datapoint3 ...

The limit is the lossless tuple. Aggregates like avg, sum, and max are morphisms out of that product—useful summaries with no universal property. Collapsing many points into one number is quotient-flavored, and quotients live on the colimit side.


EventBridge merging multiple sources:

Source1 Source2 Source3
↘ ↓ ↙
EventBridge

It’s the universal merge, identifying events by common schema.

S3 Select: Filter, Not Coequalizer

Section titled S3 Select: Filter, Not Coequalizer

It’s tempting to read filtering as identification—but nothing gets identified:

SELECT s.name FROM s3object s WHERE s.age > 21
-- WHERE carves out a subobject: the rows with age > 21
-- That's equalizer/pullback territory—limit-side, not colimit

The coequalizer would be SELECT DISTINCT s.name—quotienting rows by the kernel pair of the name projection, collapsing everything that agrees on it. Plain SELECT performs no quotient at all.

Multiple shards merged into unified stream:

Shard1 Shard2 Shard3
↘ ↓ ↙
Merged Stream

This is a colimit—the universal merge of shards.


Calculating with Limits and Colimits

Section titled Calculating with Limits and Colimits

A functor F:CDF: \mathcal{C} \to \mathcal{D} preserves limits when:

F(limD)limF(D)F(\lim D) \cong \lim F(D)

That’s a definition, not a theorem—it says what preservation means. The genuine theorem nearby: limits commute with limits, limilimjlimjlimi\lim_i \lim_j \cong \lim_j \lim_i.

Meaning: If your migration functor preserves limits, aggregations survive the migration.

Left adjoints preserve colimits. Right adjoints preserve limits.6

Meaning: If you have an adjunction between architectures, you know exactly what preserves.

// Left adjoint: "free" construction
// Preserves coproducts, pushouts, etc.
// Right adjoint: "forgetful" construction
// Preserves products, pullbacks, etc.

Designing with Limits and Colimits

Section titled Designing with Limits and Colimits

When you need to aggregate with constraints:

  1. Identify the diagram shape (what’s being combined, what constraints)
  2. Build the limit object (the aggregation point)
  3. Verify the universal property (everything factors through it)
// Stub: the limit's carrier—for a discrete diagram, the product
type LimitObject<Diagram> = { [K in keyof Diagram]: Diagram[K] };
// Generic limit pattern
interface Limit<Diagram> {
object: LimitObject<Diagram>;
projections: { [K in keyof Diagram]: (limit: LimitObject<Diagram>) => Diagram[K] };
factor: <Q>(
cone: { [K in keyof Diagram]: (q: Q) => Diagram[K] }
) => (q: Q) => LimitObject<Diagram>;
}

When you need to merge with identification:

  1. Identify the diagram shape (what’s being merged, what’s identified)
  2. Build the colimit object (the merge point)
  3. Verify the universal property (it factors to anything)
// Stub: the colimit's carrier—for a discrete diagram, the coproduct
type ColimitObject<Diagram> = Diagram[keyof Diagram];
// Generic colimit pattern
interface Colimit<Diagram> {
object: ColimitObject<Diagram>;
injections: { [K in keyof Diagram]: (d: Diagram[K]) => ColimitObject<Diagram> };
factor: <Q>(
cocone: { [K in keyof Diagram]: (d: Diagram[K]) => Q }
) => (colim: ColimitObject<Diagram>) => Q;
}

Ask of your architecture:

  • Terminal: Can everything “do nothing”?
  • Products: Can any services be aggregated?
  • Equalizers: Can computations be compared?
  • Pullbacks: Can you do consistent joins?
  • Initial: Is there an “empty” starting point?
  • Coproducts: Can any sources be merged?
  • Coequalizers: Can you quotient by equivalence?
  • Pushouts: Can you merge with identification?

If not, some constructions are impossible in your architecture. You’ll need to:

  1. Add the missing structure, or
  2. Accept the limitation, or
  3. Work in a different (more complete) architecture
Tip:

Completeness = Flexibility

A complete category can build anything. An incomplete category has fundamental limitations on what’s constructible.


Limits and colimits unify all universal constructions:

  1. Limits: Best way to project into a diagram (aggregate)
  2. Colimits: Best way to receive from a diagram (merge)
  3. Universal property: The defining characteristic—everything factors through
  4. Completeness: Having all limits/colimits means full flexibility

When designing architecture:

  • Aggregation → Limit thinking
  • Merging → Colimit thinking
  • Universal property → Correctness check

The general theory gives you the vocabulary to recognize and design any combination pattern.


Next in the series: Adjunctions: The Formal Structure of Trade-offs — Where we discover that many architectural tensions are mathematically necessary, and learn to navigate them.


  1. Formally, a cone over a diagram D:JCD: J \to \mathcal{C} is an object NN (the apex) together with a morphism ND(j)N \to D(j) for every jj in JJ, commuting with every arrow of the diagram. The limit is the terminal cone: every other cone factors through it via exactly one morphism. Terminality is why limits are unique up to unique isomorphism—two limits of the same diagram aren’t just isomorphic, they’re isomorphic in precisely one way compatible with the projections. The standard references are Mac Lane, Categories for the Working Mathematician (Springer), and Riehl, Category Theory in Context (Dover, 2016), both of which build much of the theory on this single definition.

  2. This is the payoff of Parts 8 and 9: every construction we met there is a limit or colimit for a particular choice of shape JJ. The empty shape gives the terminal object; two isolated points give the product (Part 8); the cospan \bullet \to \bullet \leftarrow \bullet gives the pullback (Part 9). Dualize—reverse every arrow—and the same shapes yield the initial object, the coproduct, and the pushout. Nothing new had to be invented; we only had to notice the parametrization.

  3. In Set, the coequalizer of f,g:ABf, g: A \to B is the quotient B/B/\sim, where \sim is the smallest equivalence relation with f(a)g(a)f(a) \sim g(a) for all aa. Its limit-side cousin is the kernel pair of a morphism f:BCf: B \to C—the pullback of ff along itself, the pairs ff cannot tell apart. In Set every surjection is the coequalizer of its own kernel pair, which is the categorical way of saying a quotient is exactly the record of what got identified. Awodey’s Category Theory (Oxford University Press) works out equalizers and coequalizers in detail with these examples.

  4. “Small” means the diagram’s shape JJ has only a set’s worth of objects and morphisms—not a proper class. The restriction isn’t pedantry: by an observation due to Peter Freyd, a small category with all small limits collapses into a preorder—at most one morphism between any two objects. So “complete” always means small-complete; a category with literally all limits would be degenerate. Set is the canonical example, complete and cocomplete for all small diagrams. Mac Lane discusses this in Categories for the Working Mathematician alongside the adjoint functor theorems, where the size condition earns its keep as Freyd’s solution set condition.

  5. DynamoDB’s TransactWriteItems executes a set of write actions as a single all-or-nothing operation—every action succeeds or the whole transaction is canceled. A transaction can span multiple tables within one AWS account and Region, currently up to 100 actions per request (the original limit of 25 was raised in 2022), with condition checks and idempotency via a client request token. What it does not give you is any cross-Region or cross-service guarantee—the atomicity boundary is the single request. See the DynamoDB transactions documentation.

  6. The mnemonic is RAPL: right adjoints preserve limits—dually, left adjoints preserve colimits. The canonical example is the free ⊣ forgetful adjunction: the forgetful functor from monoids to sets is a right adjoint, which is why the underlying set of a product of monoids is just the product of the underlying sets; the free monoid functor is a left adjoint, which is why free constructions play well with coproducts. The theorem is proved in Mac Lane’s Categories for the Working Mathematician and Awodey’s Category Theory, and Riehl’s Category Theory in Context leans on it constantly. We’ll meet adjunctions properly in the next post.

Share this post

Comments

Favorite Books

Links are Amazon affiliate links.