Decomposition — Common Mistakes and Dependency Traps
Mistake 1: Technology-Layer Decomposition
The Wrong Way
Online Store
├── Frontend
├── Backend
├── Database
└── DevOps
This isn't decomposition — it's technology labeling. It tells you what kind of code lives where, but not what the system does. You can't write a contract for "Frontend." You can't estimate "Backend." These aren't features; they're implementation categories.
Why People Do It
It's comfortable. Developers naturally think in terms of where code lives. "I'll build the frontend, you build the backend" feels like a plan. But it's not a plan — it's an organizational split that leaves all the actual design decisions unmade.
The Fix
Decompose by feature, not by technology. "Browse books" is a feature that touches frontend, backend, and database. When it's decomposed correctly, the technology concerns become implementation details within each leaf:
Online Store
├── Browse books
│ ├── List books (paginated)
│ ├── Search books
│ └── View book details
├── Shopping Cart
│ ├── Add item
│ ├── Remove item
│ └── View cart
...
Each leaf here has a clear contract, a clear estimate, and a clear set of dependencies — regardless of which technology implements it.
How to Spot It
If every branch of your tree is a technology or a "layer" rather than something a user does or the business needs, you've done technology-layer decomposition.
Mistake 2: Decomposing Too Shallow
The Wrong Way
Hospital System
├── Patient Management
├── Appointments
├── Billing
└── Records
Four branches. Each one is an entire system. "Patient Management" alone could be 50 operations. This isn't a decomposition — it's a table of contents.
Why People Do It
They stop when the branches feel "reasonable" rather than when they're concrete. "Patient Management" sounds like a reasonable module. But can you write a contract for it? Can you estimate it? No — it's still an entire subsystem compressed into two words.
The Fix
Keep asking "what does this need?" until every leaf passes the three tests (contractable, estimable, dependency-identified):
Patient Management
├── Register new patient
│ ├── Collect demographics (name, DOB, address, phone, email)
│ ├── Assign patient ID
│ ├── Verify insurance (if applicable)
│ └── Create initial medical record
├── Update patient information
│ ├── Update demographics
│ ├── Update insurance
│ └── Update emergency contact
├── Search patients
│ ├── By name
│ ├── By patient ID
│ └── By date of birth
├── Merge duplicate patient records
└── Deactivate patient record (moved away, deceased)
Now every leaf is contractable. "Collect demographics" has clear inputs, clear outputs, and a clear estimate (small).
How to Spot It
If a non-technical person can't understand what each leaf does, it's too shallow. "Patient Management" is abstract. "Register new patient" is concrete.
Mistake 3: Decomposing Too Deep
The Wrong Way
Search books by keyword
├── Receive search text from user interface
├── Trim whitespace from search text
├── Convert search text to lowercase
├── Split search text into individual words
├── Remove common words (the, a, an, is)
├── For each remaining word:
│ ├── Look up word in search index
│ ├── Retrieve list of matching book IDs
│ └── Score each match by relevance
├── Combine results from all words
├── Remove duplicate book IDs
├── Sort combined results by total relevance score
├── Fetch book details for top N results
└── Return results to user interface
This is implementation pseudocode, not decomposition. At the decomposition stage, "Search books by keyword" is a single leaf. The internal algorithm is an implementation detail.
Why People Do It
Perfectionism. The desire to have everything figured out before starting. Or anxiety about estimation — "I can't estimate 'search' unless I know exactly how it works."
The Fix
Stop decomposing when a leaf is one responsibility with a clear contract:
CONTRACT: SearchBooks
ACCEPTS: search_query (text, 1-200 characters)
RETURNS: list of matching books (title, author, price, relevance score), sorted by relevance
ERRORS: empty query, no results found
That's the leaf. How search works internally — tokenization, indexing, scoring — is decided when you implement the leaf, not when you decompose the system.
How to Spot It
If your leaves describe how something works rather than what it does, you've gone too deep. Decomposition answers "what are the pieces?" Implementation answers "how does each piece work?"
Mistake 4: Overlapping Responsibilities
The Wrong Way
E-Commerce Platform
├── Product Page
│ ├── Display product details
│ ├── Show stock availability ← checks inventory
│ └── Show recommended products
├── Shopping Cart
│ ├── Add item to cart
│ ├── Validate stock on add ← checks inventory
│ └── View cart
├── Checkout
│ ├── Reserve inventory ← modifies inventory
│ ├── Process payment
│ └── Create order
└── Admin
├── Update stock levels ← modifies inventory
└── View low-stock alerts ← checks inventory
"Inventory" appears in four different branches. Stock checking happens in Product Page, Cart, and Checkout. Stock modification happens in Checkout and Admin. If you build each branch independently, you'll build the inventory logic four different times — with four different behaviors.
The Fix
Identify the shared responsibility and make it explicit:
E-Commerce Platform
├── Inventory (shared)
│ ├── Check stock level
│ ├── Reserve stock (temporary hold)
│ ├── Confirm reservation (convert to deduction)
│ ├── Release reservation (timeout or cancel)
│ └── Adjust stock (admin)
├── Product Page → uses Inventory.CheckStock
├── Shopping Cart → uses Inventory.CheckStock
├── Checkout → uses Inventory.Reserve, then Inventory.Confirm
└── Admin → uses Inventory.Adjust, Inventory.CheckStock
Now inventory is decomposed once and referenced by the branches that need it. The dependency is explicit.
How to Spot It
If the same verb + noun appears in multiple branches ("check stock," "validate user," "calculate price"), it's an overlap. Extract it as a shared dependency.
Mistake 5: Missing the Unhappy Path
The Wrong Way
Flight Booking
├── Search flights
├── Select flight
├── Enter passenger details
├── Pay
└── Issue ticket
Five steps. All happy path. But what about:
- Flight sells out between search and payment?
- Payment is declined?
- Passenger name doesn't match their ID?
- Flight is canceled after booking?
- Customer wants to change their flight?
- Customer wants a refund?
The unhappy paths are at least as numerous as the happy path, often more.
The Fix
For every happy-path branch, ask: "What can go wrong, and what do we do about it?"
Flight Booking
├── Search flights
├── Select flight
│ └── Handle: flight no longer available → show alternatives
├── Enter passenger details
│ └── Handle: validation failures → show field-level errors
├── Pay
│ ├── Handle: payment declined → retry or try different card
│ ├── Handle: flight sold out during payment → refund, show alternatives
│ └── Handle: payment timeout → check if payment went through, avoid double charge
├── Issue ticket
│ └── Handle: system error after payment → queue for retry, notify customer of delay
├── Post-Booking
│ ├── Cancel booking → calculate refund based on fare rules
│ ├── Change flight → calculate fare difference
│ ├── Flight canceled by airline → auto-rebook or refund
│ └── Schedule change by airline → notify and offer alternatives
The tree roughly doubled. That's normal for real systems — the unhappy paths are half the work.
How to Spot It
If your decomposition tree reads like a tutorial ("step 1, step 2, step 3...") with no branching, you've only captured the happy path. Real systems branch extensively.
Mistake 6: Circular Dependencies
The Wrong Way
Module A (User Profiles) needs Module B (Permissions) to check if user can edit profiles
Module B (Permissions) needs Module A (User Profiles) to look up user's role
A depends on B. B depends on A. Neither can be built first. Neither can be tested alone. This is a circular dependency — and it's always a decomposition error.
Why It Happens
Two things that are related get decomposed as peers that reference each other. In reality, one should depend on the other, or both should depend on a third, more fundamental thing.
The Fix
Find the deeper abstraction:
Module C (User Data) ← stores user ID, role, basic profile data (no logic)
│
├────────────────┐
▼ ▼
Module A (Profiles) Module B (Permissions)
uses User Data uses User Data
Now both A and B depend on C, but not on each other. The cycle is broken.
How to Spot It
Draw your dependency arrows. If you can follow the arrows in a circle (A → B → C → A), you have a cycle. Every cycle must be broken by extracting the shared dependency.
The Dependency Health Checklist
After decomposing, validate your dependency structure:
| Check | What to Look For |
|---|---|
| No cycles | Can you sort all modules in a build order where each module only depends on things above it? |
| Shared responsibilities are explicit | Is any logic duplicated across branches? Extract it. |
| Foundation modules depend on nothing | Your data stores, core entities, and configuration should be at the bottom of the dependency graph. |
| High-level features depend on low-level services | "Checkout" depends on "Inventory" and "Payment" — not the reverse. |
| Every dependency is justified | For each arrow, can you explain why it exists? If not, it might be artificial. |
| It's possible to build and test each piece independently | If you can't build module X without also building module Y, either X depends on Y (document it) or they should be combined. |