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:

CheckWhat to Look For
No cyclesCan you sort all modules in a build order where each module only depends on things above it?
Shared responsibilities are explicitIs any logic duplicated across branches? Extract it.
Foundation modules depend on nothingYour 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 justifiedFor each arrow, can you explain why it exists? If not, it might be artificial.
It's possible to build and test each piece independentlyIf you can't build module X without also building module Y, either X depends on Y (document it) or they should be combined.