Boundaries — Common Mistakes

The Five Antipatterns

After seeing how boundaries should work across three examples, let's look at how they go wrong. These mistakes are so common that you will encounter every one of them in your career. Recognizing them is half the battle.


Mistake 1: The God Module

What It Looks Like

One module grows to handle a massive portion of the system. It started small and reasonable, then feature after feature was added because "it's related" or "it's easier to put it here."

Before (Bad):

OrderService
├── Create order
├── Calculate totals
├── Apply discount codes
├── Validate inventory
├── Reserve stock
├── Process payment
├── Process refund
├── Generate invoice
├── Send confirmation email
├── Send shipping notification
├── Update order status
├── Track shipment
├── Handle returns
├── Generate sales reports
└── Manage customer loyalty points

15 responsibilities. This module is impossible to name accurately — "OrderService" doesn't cover half of what it actually does. Any change to any of these responsibilities risks breaking all the others.

How to Spot It

  • The module has more than 5-7 responsibilities
  • Its name doesn't accurately describe everything inside
  • Changes to the module are frequent and scary
  • Multiple developers are constantly working in the same module and colliding
  • Testing requires setting up the entire system because everything is connected

After (Fixed):

Orders                     Pricing                Payment
├── Create order           ├── Calculate totals   ├── Charge
├── Update status          ├── Apply discounts    ├── Refund
├── Track history          └── Tax calculation    └── Payment history
└── Cancel order

Inventory                  Fulfillment            Communication
├── Check stock            ├── Ship order         ├── Email templates
├── Reserve stock          ├── Track shipment     ├── Send confirmation
└── Release reservation    └── Process return     └── Send notifications

Billing                    Loyalty
├── Generate invoice       ├── Earn points
└── Payment tracking       └── Redeem points

Same functionality. Eight modules instead of one. Each with 2-4 responsibilities. Each with a clear name. Each changeable independently.


Mistake 2: The Micro-Boundary

What It Looks Like

The opposite of the God Module. Everything is its own boundary, each with trivial responsibility.

Before (Bad):

EmailValidator           ← validates email format
PasswordValidator        ← validates password strength
NameValidator            ← validates name length
AddressValidator         ← validates address format
PhoneValidator           ← validates phone format
DateValidator            ← validates date format
LoginHandler             ← handles login
LogoutHandler            ← handles logout
SessionCreator           ← creates sessions
SessionDestroyer         ← destroys sessions
PasswordHasher           ← hashes passwords
TokenGenerator           ← generates auth tokens

12 modules for what is clearly one concern: Authentication.

How to Spot It

  • You have modules with only 1-2 functions
  • Many modules always change together (if EmailValidator changes, LoginHandler probably does too)
  • Understanding a single feature requires reading 10 modules
  • The connection diagram looks like a plate of spaghetti

After (Fixed):

Authentication
├── Validate credentials (email, password, etc.)
├── Login / Logout
├── Session management
├── Password hashing
└── Token generation

One module. Five cohesive responsibilities. All related to "verifying and managing user identity." If someone asks "where is the login logic?" the answer is one word: Authentication.

The Test

If two things always change together, they probably belong together. Micro-boundaries violate cohesion — they separate things that should be unified.


Mistake 3: Boundaries Follow Technology, Not Domain

What It Looks Like

DatabaseModule          ← all database operations for all features
APIModule               ← all API endpoints for all features
UIModule                ← all user interface code for all features

Why It's Wrong

Where does "checkout" live? Partly in the API (the checkout endpoint), partly in the Database (saving the order), partly in the UI (the checkout page). The checkout logic is scattered across three modules. To understand checkout, you must read all three.

Where does "user registration" live? Also spread across all three modules. Now checkout and registration code live side-by-side in each module, even though they have nothing to do with each other.

The Result

  • Low cohesion: each module contains unrelated things (checkout + registration + search + ... all in the same "database module")
  • High coupling: changing checkout requires changing three modules simultaneously
  • Impossible to reason about: "where is the checkout logic?" → "everywhere"

After (Fixed):

Checkout                    Registration              Search
├── Checkout API endpoint   ├── Registration API      ├── Search API
├── Checkout database ops   ├── Registration DB ops   ├── Search index ops
└── Checkout UI page        └── Registration UI page  └── Search UI component

Each module contains everything needed for its domain — the API, the data access, and the UI. Now changing checkout only touches the Checkout module.

The Principle

Boundaries should follow the domain (what the system does), not the technology (how it's built). "Checkout" is a domain boundary. "Database" is a technology boundary. Domain boundaries create high cohesion. Technology boundaries create high coupling.


Mistake 4: The Shared Junk Drawer

What It Looks Like

Utils/
├── formatDate()
├── calculateShipping()
├── validateEmail()
├── generatePDF()
├── checkUserPermissions()
├── convertCurrency()
├── sendSlackMessage()
├── compressImage()
├── parseCSV()
└── retryWithBackoff()

Why It's Wrong

"Utils" is not a responsibility. It's a confession that nobody thought about where these things should live. Each function belongs somewhere:

FunctionActually Belongs In
formatDate()Whichever module needs it, as a private helper. Or a shared "Date/Time" utility if multiple modules truly need the same formatting.
calculateShipping()Fulfillment or Pricing
validateEmail()Authentication or Accounts
generatePDF()Billing (for invoices) or Reporting
checkUserPermissions()Authentication/Authorization
convertCurrency()Pricing
sendSlackMessage()Communication/Notifications
compressImage()Media/Content processing
parseCSV()Import/Data Processing
retryWithBackoff()This is genuinely cross-cutting — it's a shared infrastructure utility

The Damage

  • Half the system depends on "Utils," creating hidden coupling
  • Changing any function risks breaking modules you didn't know used it
  • The module grows without limit — there's no criteria for what should or shouldn't be in it
  • New developers dump everything there because it's the path of least resistance

After (Fixed):

Move each function to the module it actually belongs to. For the genuinely cross-cutting pieces (retry logic, date formatting if truly universal), create a named infrastructure module:

Infrastructure/Resilience    ← retryWithBackoff()
Infrastructure/Formatting    ← formatDate(), formatCurrency() (if truly shared)

These have names. They have boundaries. They are not growing junk drawers.


Mistake 5: Hidden Cross-Boundary Coupling

What It Looks Like

The modules look clean on the org chart, but they secretly share:

Shared database tables. Module A and Module B both read from and write to the same table. Neither "owns" it. If A changes the table structure, B breaks.

Shared data models. Both modules use the same internal data structures. Changing the structure in one requires changing the other.

Behavior assumptions. Module A depends on Module B processing items in a specific order, but that order isn't in the contract — it's just how B happens to work today. When B is optimized to process in a different order, A breaks.

A Concrete Example

Orders module and Shipping module both access the orders table directly.

Orders Module ──writes──► ┌─────────┐ ◄──reads── Shipping Module
                          │ orders  │
                          │  table  │
                          └─────────┘

This seems efficient. But:

  • Orders adds a new column → Shipping might break if it does SELECT *
  • Orders changes the status values from "ready" to "awaiting_shipment" → Shipping was filtering on "ready" and stops seeing orders
  • Shipping updates the tracking number directly in the orders table → Orders doesn't know it happened and might overwrite it

After (Fixed):

Orders Module                        Shipping Module
      │                                    ▲
      │  "here are orders                  │
      │   ready to ship"                   │
      ▼                                    │
┌──────────────────────────────────────────┐
│           Defined Contract               │
│  Orders provides: order_id, items,       │
│    shipping address, priority            │
│  Shipping returns: tracking_number,      │
│    estimated delivery date               │
└──────────────────────────────────────────┘

Each module owns its own data storage. Communication happens through defined contracts. Neither module needs to know how the other stores its data.


How to Detect These Mistakes in Any System

Question to AskWhat a Bad Answer Reveals
"Can you explain this module in one sentence?"God Module (the sentence uses "and" five times) or Micro-Boundary (the sentence is trivially short)
"If I change the inside of this module, what else breaks?"Hidden coupling (anything other than "nothing" is concerning)
"What's in the Utils/Helpers/Common module?"Junk drawer (if the answer takes more than 60 seconds, it's too big)
"Where does feature X live?"Technology-based boundaries (if the answer is "parts of it are in three different modules")
"Do any two modules read from the same database table?"Shared data coupling
"When was the last time you changed this module without fear?"God Module or coupling (if the answer is "never," there's a problem)