Axiom IV: Composition Over Monoliths
Imagine you inherit a codebase. Your task: add email notifications when a user completes an order. You open the main file and find process_order()—a 2,000-line function. It handles validation, inventory checks, payment processing, shipping calculations, tax computation, receipt generation, loyalty point updates, and analytics logging. All in one function. All interleaved. Changing any piece risks breaking everything else.
Where do you add email notifications? After payment but before shipping? Between receipt generation and loyalty points? Every insertion point touches code that does twelve other things. You test your change, and suddenly tax calculations produce wrong numbers because you accidentally moved a variable assignment three hundred lines above.
Now imagine the alternative. The same logic exists as focused units: validate_order(), check_inventory(), process_payment(), calculate_shipping(), generate_receipt(). Each does one thing. Each has clear inputs and outputs. Adding email notifications means composing a new unit—send_notification()—into the pipeline. Nothing else changes. Nothing else can break.
This is Axiom IV: Composition Over Monoliths—the architectural principle that complex systems are built from small, focused units that communicate through well-defined interfaces.
The Problem Without This Axiom
Without composition, software grows like a tangled vine. Each new feature weaves deeper into existing code. Each change requires understanding the entire system. Each bug hides behind layers of unrelated logic.
Consider what happens to a monolithic system over time:
| Month | What Happens | Consequence |
|---|---|---|
| 1 | Single function works perfectly for initial scope | Developer feels productive |
| 3 | New requirements added inside the function | Function grows to 300 lines |
| 6 | Bug fix touches unrelated code paths | Regression in seemingly unrelated feature |
| 9 | New developer joins, cannot understand the function | Onboarding takes weeks instead of days |
| 12 | AI assistant asked to modify function | AI hallucinates because context exceeds useful window |
| 18 | Feature request requires architectural change | "We need to rewrite everything" |
The trajectory is predictable. Monoliths start convenient and become unmaintainable. Composed systems start with slightly more structure and remain maintainable indefinitely.
The Axiom Defined
Axiom IV: Composition Over Monoliths
Complex systems are built from composable, focused units. Each unit does one thing well. Units communicate through well-defined interfaces. The Unix philosophy applied to software architecture.
Three properties define a composable unit:
- Focused: It does one thing and does it completely
- Interface-defined: Its inputs and outputs are explicit and typed
- Independent: It can be tested, understood, and replaced without touching other units
When these properties hold, units compose naturally—like LEGO bricks that snap together in countless configurations, each brick useful on its own but powerful in combination.
From Principle to Axiom
In Chapter 4, you learned Principle 4: Small, Reversible Decomposition—breaking problems into atomic steps that can be independently verified and rolled back. That principle governs your process: how you approach solving problems.
Axiom IV governs your architecture: how you structure the solutions themselves.
| Aspect | Principle 4 (Process) | Axiom IV (Architecture) |
|---|---|---|
| Focus | How you work | What you build |
| Unit | A commit, a step | A function, a module |
| Goal | Manageable progress | Maintainable systems |
| Reversibility | Git revert a step | Swap out a component |
| Scale | Task decomposition | System decomposition |
The principle says: "Break your work into small steps." The axiom says: "Build your systems from small parts." One is about the journey; the other is about the destination. Together, they ensure both your process and your product remain manageable.
The Unix Philosophy: Where This Began
In 1978, Doug McIlroy articulated what became the Unix philosophy:
Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.
This philosophy produced tools that have endured for over forty years: grep finds patterns, sort orders lines, wc counts words, head takes the first N lines. Each is simple. Each is composable. Together, they solve problems their creators never imagined:
# Find the 5 most common error types in a log file
cat server.log | grep "ERROR" | cut -d: -f2 | sort | uniq -c | sort -rn | head -5
No single tool solves this problem. But composed together through the pipe operator (|), six simple tools produce a powerful analysis pipeline. Each tool receives text, transforms it, and outputs text. The pipe is the universal interface.
This is not historical trivia—it is the architectural pattern that makes AI-native development possible.
Composition at Every Scale
The Unix philosophy applies at every level of software, from individual functions to distributed systems.
Scale 1: Functions
The smallest unit of composition is the function. Compare these approaches:
Monolithic approach:
Loading Python environment...
This function does five things: validation, hashing, storage, email, and logging. Testing any one behavior requires executing all of them. Changing email logic risks breaking validation. An AI asked to "add phone number verification" must understand all 70+ lines of context.
Composed approach:
Loading Python environment...
Now each function does one thing. Each can be tested independently. Each can be replaced without touching the others. The orchestrating function register_user() reads like a recipe—a sequence of composed steps.
Scale 2: Modules
Functions compose into modules. Each module groups related functions around a single domain concept:
user_management/
__init__.py
validation.py # validate_registration, validate_password_strength
security.py # hash_password, verify_password, generate_token
storage.py # store_user, get_user, update_user
notifications.py # send_verification_email, send_welcome_email
registration.py # register_user (orchestrates the above)
Each module can be imported independently. Testing validation.py never touches the database. Replacing the email provider means changing only notifications.py. An AI assistant can work within a single module without needing context from the others.
Scale 3: Packages and Services
Modules compose into packages. Packages compose into services. The same principle applies at every level:
Order System (composed of services)
├── auth-service/ → Handles identity and permissions
├── catalog-service/ → Manages product information
├── payment-service/ → Processes transactions
├── notification-service/→ Sends emails and alerts
└── order-service/ → Orchestrates the order workflow
Each service does one thing. Each communicates through defined interfaces (APIs). Each can be developed, deployed, and scaled independently. The pattern is fractal—the same structure repeats at every scale.
Why AI Loves Composition
Composition is not merely a human preference for clean code. It is an architectural requirement for effective AI collaboration. Here is why:
Context Windows Are Finite
Every AI model has a limited context window—the amount of text it can consider at once. When your code is monolithic, the AI must load the entire monolith to understand any part of it. When your code is composed, the AI loads only the unit it needs.
| Structure | Context Required | AI Effectiveness |
|---|---|---|
| 2000-line monolith | Full 2000 lines | Poor: important details get lost in noise |
| 20 composed functions | 30-80 lines per function | Excellent: full context of the unit fits easily |
Focused Generation Produces Better Results
When you ask an AI to "fix the payment processing bug in this 2000-line function," it must find the payment logic among validation, shipping, and analytics code. It might accidentally modify the wrong section. It might miss relevant context buried 800 lines away.
When you ask an AI to "fix the bug in process_payment()" and that function is 40 lines long, the AI has complete, focused context. Its generation is more accurate because its attention is not diluted.
Composable Units Are Independently Testable
AI-generated code needs verification. With monolithic code, testing requires setting up the entire system state. With composed units, you test each unit in isolation:
Loading Python environment...
No database setup. No email server. No authentication state. Just the function and its expected behavior.
AI Can Replace Units Without Breaking the Whole
The most powerful property of composition for AI collaboration: any unit can be regenerated independently. If an AI produces a poor implementation of hash_password(), you replace just that function. The rest of the system remains untouched. This makes AI-assisted development iterative and safe—you improve one piece at a time, verifying each change in isolation.
Dependency Injection: Composition of Behavior
Dependency injection is composition applied to behavior. Instead of hardcoding which specific implementation a function uses, you pass the implementation as a parameter:
Without dependency injection (hardcoded dependency):
Loading Python environment...
With dependency injection (composable behavior):
Loading Python environment...
Now the same orchestration function works with different implementations:
Loading Python environment...
The function's behavior is composed from the implementations you provide. This is the Unix philosophy at the code level: focused units connected through interfaces.
The Pipe Operator as Architectural Metaphor
In Unix, the pipe (|) connects programs: the output of one becomes the input of the next. This creates data pipelines—sequences of transformations applied to flowing data.
The same pattern appears in well-composed Python code:
Loading Python environment...
Each step takes data, transforms it, and passes the result forward. Each step is a focused unit. Adding a new transformation (discount calculation, loyalty points) means inserting one line—not rewriting the pipeline.
For a more explicit pipeline pattern:
Loading Python environment...
This is composition made visible: the system is literally a sequence of composed functions, each doing one thing well.
Composition in the AI Era
The composition principle extends beyond traditional code. In AI-native development, the same pattern appears at new scales:
Skills compose into agents. A skill is a focused unit of expertise—like a function that does one thing well. An agent orchestrates multiple skills, like register_user() orchestrates validation, hashing, and storage.
Agents compose into workflows. Multiple agents collaborate on complex tasks, each handling its domain. A planning agent produces a specification. An implementation agent writes code. A validation agent verifies quality. The workflow is a pipeline of composed agents.
Prompts compose into conversations. Rather than one massive prompt trying to accomplish everything, effective AI collaboration uses composed prompts—each focused on one concern, each building on the output of the previous.
The pattern is universal: focused units, clear interfaces, flexible composition.
Anti-Patterns: What Composition Violations Look Like
Recognizing anti-patterns is as important as understanding the principle. Here are the most common composition violations:
| Anti-Pattern | Symptom | Consequence | Composed Alternative |
|---|---|---|---|
| God Class | One class with 50+ methods handling unrelated concerns | Changes to any feature risk breaking all others | Split into focused classes, each with a single responsibility |
| Monolithic Function | 500+ line function with multiple responsibilities | Cannot test, understand, or modify in isolation | Extract focused helper functions with clear interfaces |
| Tight Coupling | Module A directly imports internals of Module B | Changes to B cascade as breaking changes to A | Define interfaces; A depends on the interface, not B's internals |
| Copy-Paste Reuse | Same logic duplicated in 5 places | Bug fix must be applied 5 times; one is always missed | Extract to shared function; compose where needed |
| Circular Dependencies | Module A imports B, B imports A | Cannot understand either module in isolation; import errors | Extract shared logic to Module C; both A and B import C |
| Hidden State | Functions modify global variables instead of returning values | Unpredictable behavior; testing requires resetting global state | Pure functions that take inputs and return outputs |
Spotting the God Class
Loading Python environment...
This class has no single responsibility. It is the entire application stuffed into one object. Testing payment processing requires instantiating a class that also handles email, reports, and inventory.
Loading Python environment...
Each class is testable in isolation. Each can evolve independently. An AI assistant can work on PaymentProcessor without needing context about notifications or inventory.
The Composition Test
When evaluating code—whether yours, a teammate's, or AI-generated—apply this test:
- Can I explain this unit in one sentence? If not, it does too much.
- Can I test this unit without setting up unrelated systems? If not, it has hidden dependencies.
- Can I replace this unit without modifying other units? If not, coupling is too tight.
- Can I reuse this unit in a different context? If not, it contains unnecessary specifics.
If any answer is "no," the code needs decomposition. Break it into smaller units until every answer is "yes."
Safety Note
Composition is a spectrum, not a binary. Over-decomposition creates its own problems: too many tiny functions make code harder to follow, excessive abstraction layers obscure simple logic, and premature generalization wastes effort on flexibility you never need.
The goal is not maximum decomposition—it is appropriate decomposition. A 20-line function that does one clear thing does not need to be split into four 5-line functions. A simple script that runs once does not need a plugin architecture.
Apply composition when:
- A function does multiple unrelated things
- You cannot test a behavior without setting up unrelated state
- Changes to one concern break unrelated concerns
- Multiple places need the same logic (duplication signals missing composition)
Do not apply composition when:
- The code is simple and unlikely to change
- The abstraction would be more complex than the duplication
- You are optimizing for a future that may never arrive
Try With AI
Prompt 1: Refactor a Monolith
Here is a monolithic function. Help me decompose it into composable units.
[Paste a long function from your own code, or use this example:]
def process_csv_report(filepath):
# Read file
with open(filepath) as f:
lines = f.readlines()
# Parse headers
headers = lines[0].strip().split(',')
# Parse rows
rows = []
for line in lines[1:]:
values = line.strip().split(',')
row = dict(zip(headers, values))
rows.append(row)
# Filter valid rows
valid = [r for r in rows if r.get('status') == 'active']
# Calculate totals
total = sum(float(r['amount']) for r in valid)
# Format output
report = f"Active records: {len(valid)}\nTotal amount: ${total:.2f}"
# Write report
with open('report.txt', 'w') as f:
f.write(report)
return report
For each composed unit you extract:
1. What is its single responsibility?
2. What are its inputs and outputs (the interface)?
3. How would you test it independently?
4. Could an AI regenerate just this unit without affecting the rest?
What you're learning: The practical skill of identifying composition boundaries in real code. You are developing an eye for where responsibilities separate and where interfaces naturally emerge—the core skill for writing AI-friendly, maintainable code.
Prompt 2: Design an Interface
I want to understand dependency injection and interface-based design.
Take this tightly coupled function:
def save_user_data(user):
db = PostgresConnection("localhost", 5432, "mydb")
db.insert("users", user)
logger = FileLogger("/var/log/app.log")
logger.info(f"User {user['name']} saved")
emailer = SmtpClient("smtp.gmail.com", 587)
emailer.send(user['email'], "Welcome!", "Account created.")
Help me redesign this so that:
- The storage mechanism is injectable (could be Postgres, SQLite, or in-memory)
- The logging mechanism is injectable (could be file, console, or nothing)
- The notification mechanism is injectable (could be email, SMS, or a test stub)
Show me:
1. The interface each dependency should satisfy
2. The refactored function using dependency injection
3. Three different compositions: production, testing, development
4. Why this makes the code more AI-friendly
What you're learning: How to decouple behavior from implementation through interfaces and dependency injection. You are learning to think about what a component needs (its interface) separately from how that need is fulfilled (its implementation)—a fundamental skill for composable architecture.
Prompt 3: Composition in Your Domain
I work in [describe your domain: web development, data science, DevOps, mobile apps, etc.].
Help me apply the Composition Over Monoliths axiom to my specific context:
1. What are the "focused units" in my domain?
(In web dev: components, middleware, routes. In data science: transforms, models, pipelines.)
2. What are the "interfaces" between units?
(In web dev: props, request/response. In data science: DataFrames, arrays.)
3. What does a "god class" look like in my domain?
(Show me a realistic anti-pattern I might encounter.)
4. What does a well-composed system look like in my domain?
(Show me the same functionality decomposed into focused units.)
5. How does composition specifically help AI tools in my domain?
(What can an AI do better when my code is composed vs. monolithic?)
Use concrete examples from [my specific technology stack or project type].
What you're learning: How to translate the universal principle of composition into the specific patterns and practices of your domain. Every field has its own version of "focused units" and "interfaces"—learning to recognize yours is what transforms abstract knowledge into practical skill.