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 PatternsLook at what we’ve covered:
| Construction | What It Does | Universal Property |
|---|---|---|
| Product A × B | Combines A and B | Best way to access both |
| Coproduct A + B | Offers choice of A or B | Best way to accept either |
| Pullback | Combines A and B consistently over C | Best consistent combination |
| Pushout | Merges A and B identifying along C | Best identified merge |
| Terminal object 1 | The “single point” | Unique morphism from anything |
| Initial object 0 | The “empty” object | Unique 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 ConstructionA limit is the most general way to “combine” objects subject to constraints.1
Given a diagram (a shape mapped into your category), the limit is:
- An object with morphisms to each object in the diagram
- Such that all triangles commute (constraints are satisfied)
- With the universal property: any other such object factors uniquely through it
Visualizing Limits
Section titled Visualizing Limits 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.
Special Cases of Limits
Section titled Special Cases of LimitsTerminal Object (Empty Diagram)
Section titled Terminal Object (Empty Diagram)When is empty, the limit is the terminal object :
// Terminal object: unit typetype Unit = void;
// Unique morphism from any typeconst toUnit = <A>(a: A): Unit => undefined;Architectural meaning: The “trivial” service that everything can ignore.
Product (Discrete Diagram)
Section titled Product (Discrete Diagram)When has two objects with no morphisms between them:2
J: • • (two disconnected points)
lim = A × B with projections π₁, π₂Architectural meaning: Gateway combining independent services.
Equalizer (Parallel Arrows)
Section titled Equalizer (Parallel Arrows)When 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 agreedeclare 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;};Pullback (Cospan)
Section titled Pullback (Cospan)When has the shape :
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 ConstructionColimits are dual to limits—they’re the “best” way to merge or combine by identification.
Given a diagram , the colimit is:
- An object with morphisms FROM each object in the diagram
- Such that all triangles commute
- Universal: for any other cocone with apex , there is a unique morphism commuting with the cocone legs
Visualizing Colimits
Section titled Visualizing Colimits D(4) ↗ ↑ ↖ D(1) D(2) D(3) ↘ ↓ ↙ colim DThe colimit sits “below” the diagram, receiving from each piece.
Special Cases of Colimits
Section titled Special Cases of ColimitsInitial Object (Empty Diagram)
Section titled Initial Object (Empty Diagram)When is empty, the colimit is the initial object :
// Initial object: never/void typetype Never = never;
// Unique morphism to any typeconst fromNever = <A>(n: Never): A => n; // absurdArchitectural meaning: The “impossible” service that can produce anything (because it never runs).
Coproduct (Discrete Diagram)
Section titled Coproduct (Discrete Diagram)When has two disconnected objects:
J: • •
colim = A + B with injections ι₁, ι₂Architectural meaning: Event router accepting from multiple sources.
Coequalizer (Parallel Arrows)
Section titled Coequalizer (Parallel Arrows)When 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 equivalencetype EquivalenceClass<B> = { representative: B };
// Identify f(a) with g(a) for all a; map each b to its classdeclare const coequalizer: <A, B>( f: (a: A) => B, g: (a: A) => B) => (b: B) => EquivalenceClass<B>;Pushout (Span)
Section titled Pushout (Span)When has the shape :
J: A ← C → B
colim = A +_C B (pushout)Architectural meaning: Merge identifying common structure.
The Limit-Colimit Dictionary
Section titled The Limit-Colimit Dictionary| Limit | Colimit | Relationship |
|---|---|---|
| Terminal object 1 | Initial object 0 | Unique in/out |
| Product A × B | Coproduct A + B | Combine/Choose |
| Equalizer | Coequalizer | Agree/Identify |
| Pullback | Pushout | Consistent combine/Merge |
| Limit | Colimit | Project into/Receive from |
Complete and Cocomplete Categories
Section titled Complete and Cocomplete CategoriesA 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: Unittype Unit = void;
// Products: tuplestype Product<A, B> = [A, B];
// Equalizers: subsetstype Equalizer<A, B> = A & { _eq: true }; // Conceptually
// All small colimits exist too
// Initial: Nevertype Never = never;
// Coproducts: unionstype Coproduct<A, B> = A | B;
// Coequalizers: quotients (harder to express)Your Architecture Category?
Section titled Your Architecture Category?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.
Limits in AWS
Section titled Limits in AWSAPI Gateway as Limit
Section titled API Gateway as LimitAn API Gateway over multiple Lambda functions is a limit:
API Gateway ↙ ↓ ↘ Lambda1 Lambda2 Lambda3It’s the universal way to access all three consistently.
DynamoDB Transactions: Joint Consistency
Section titled DynamoDB Transactions: Joint ConsistencyTransactWriteItems 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.CloudWatch Metrics and Limits
Section titled CloudWatch Metrics and LimitsThe 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.
Colimits in AWS
Section titled Colimits in AWSEventBridge as Colimit
Section titled EventBridge as ColimitEventBridge merging multiple sources:
Source1 Source2 Source3 ↘ ↓ ↙ EventBridgeIt’s the universal merge, identifying events by common schema.
S3 Select: Filter, Not Coequalizer
Section titled S3 Select: Filter, Not CoequalizerIt’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 colimitThe 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.
Kinesis Stream Merge
Section titled Kinesis Stream MergeMultiple shards merged into unified stream:
Shard1 Shard2 Shard3 ↘ ↓ ↙ Merged StreamThis is a colimit—the universal merge of shards.
Calculating with Limits and Colimits
Section titled Calculating with Limits and ColimitsFunctors That Preserve Limits
Section titled Functors That Preserve LimitsA functor preserves limits when:
That’s a definition, not a theorem—it says what preservation means. The genuine theorem nearby: limits commute with limits, .
Meaning: If your migration functor preserves limits, aggregations survive the migration.
Colimits and Adjoints
Section titled Colimits and AdjointsLeft 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 ColimitsThe Limit Design Pattern
Section titled The Limit Design PatternWhen you need to aggregate with constraints:
- Identify the diagram shape (what’s being combined, what constraints)
- Build the limit object (the aggregation point)
- Verify the universal property (everything factors through it)
// Stub: the limit's carrier—for a discrete diagram, the producttype LimitObject<Diagram> = { [K in keyof Diagram]: Diagram[K] };
// Generic limit patterninterface 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>;}The Colimit Design Pattern
Section titled The Colimit Design PatternWhen you need to merge with identification:
- Identify the diagram shape (what’s being merged, what’s identified)
- Build the colimit object (the merge point)
- Verify the universal property (it factors to anything)
// Stub: the colimit's carrier—for a discrete diagram, the coproducttype ColimitObject<Diagram> = Diagram[keyof Diagram];
// Generic colimit patterninterface 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;}The Completeness Question
Section titled The Completeness QuestionAsk of your architecture:
Does it have all limits?
Section titled Does it have all limits?- Terminal: Can everything “do nothing”?
- Products: Can any services be aggregated?
- Equalizers: Can computations be compared?
- Pullbacks: Can you do consistent joins?
Does it have all colimits?
Section titled Does it have all colimits?- 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:
- Add the missing structure, or
- Accept the limitation, or
- Work in a different (more complete) architecture
Completeness = Flexibility
A complete category can build anything. An incomplete category has fundamental limitations on what’s constructible.
The Takeaway
Section titled The TakeawayLimits and colimits unify all universal constructions:
- Limits: Best way to project into a diagram (aggregate)
- Colimits: Best way to receive from a diagram (merge)
- Universal property: The defining characteristic—everything factors through
- 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.
Footnotes
Section titled FootnotesFootnotes
Section titled Footnotes-
Formally, a cone over a diagram is an object (the apex) together with a morphism for every in , 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. ↩
-
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 . The empty shape gives the terminal object; two isolated points give the product (Part 8); the cospan 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. ↩
-
In Set, the coequalizer of is the quotient , where is the smallest equivalence relation with for all . Its limit-side cousin is the kernel pair of a morphism —the pullback of along itself, the pairs 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. ↩
-
“Small” means the diagram’s shape 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. ↩
-
DynamoDB’s
TransactWriteItemsexecutes 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. ↩ -
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. ↩