module-boundaries
Module Boundaries
Diagnose and fix modules that have grown beyond a single clear purpose, or that have become so entangled with other modules that changes ripple unpredictably. The goal: each module owns one domain of logic, exposes only what callers need, and can be read, tested, and changed without understanding the rest of the system.
When to Apply This Skill
Trigger on any of:
- A single file/module exceeds ~300 lines and handles multiple distinct concerns
- Circular imports between two or more modules
- A module importing from many unrelated parts of the codebase
- Changes to one module routinely break unrelated modules
- "Utils", "helpers", "common", or "shared" modules that have become dumping grounds
- No clear distinction between what a module exposes publicly vs. uses internally
- Multiple modules duplicating the same logic because there's no clear owner
Step 1 — Map the Current State
Before proposing any structure, understand what exists. Gather:
1. The import graph. Who imports whom? Look for:
- Modules imported by many others (high fan-in) — potential shared kernel or over-coupled util
- Modules that import many others (high fan-out) — potential god module or orchestration layer
- Cycles — A imports B imports A
Run this to get a quick picture (Node.js example):
# Show what each file imports
grep -r "^import\|^const.*require" src/ --include="*.js" --include="*.ts" -l | \
xargs -I{} sh -c 'echo "=== {} ==="; grep -E "^import|require\(" {}'
2. The responsibility inventory. For each module, write one sentence: "This module is responsible for ___." If you need "and" more than once, it's doing too much.
3. Coupling hotspots. Note any module where:
- The import list is longer than ~8 entries
- It imports from both
domain/andinfrastructure/layers (mixed abstraction) - It's imported in both UI and backend code (mixed context)
Step 2 — Diagnose the Coupling Type
Different coupling problems need different fixes:
| Problem | Symptom | Fix |
|---|---|---|
| God module | One module does everything: types, logic, DB access, formatting | Split by domain concern |
| Dumping ground | utils.ts or helpers/ with 30 unrelated functions |
Redistribute to owning modules; create domain-specific util files |
| Circular dependency | A → B → A (often caused by shared types or event handlers) | Extract shared types to a neutral module; invert one dependency |
| Leaky internals | Callers import and use private implementation details | Define an explicit public interface (index.ts barrel or __init__.py) |
| Cross-layer imports | UI importing DB models directly; domain importing HTTP request objects | Introduce a boundary type / DTO at each layer edge |
| Premature unification | Two features merged into one module because they seemed similar | Split by feature, not by type (prefer vertical slices) |
Step 3 — Design the New Boundaries
The core principle: cohesion over convenience
A module boundary should reflect a domain boundary, not a file-size limit. Ask:
- Would a new team member know where to find this logic without a search?
- Can this module be tested without instantiating anything from another module?
- If this module changes, is the set of things that need to change clearly scoped?
Boundary archetypes
Feature module (vertical slice)
orders/
├── index.ts ← public interface (only export from here)
├── order.model.ts ← domain types
├── order.service.ts ← business logic
├── order.repo.ts ← data access
└── order.schema.ts ← validation
Everything about orders lives together. Other modules import only from orders/index.ts.
Shared kernel (horizontal slice — use sparingly)
shared/
├── types/ ← domain primitives used across features (UserId, Money, etc.)
├── errors/ ← base error classes
└── events/ ← event bus types (not implementations)
The shared kernel should be minimal and stable. If it changes often, it's not a shared kernel — it's a coupling hotspot.
Infrastructure module
infra/
├── db/ ← DB client, connection, base repo
├── email/ ← email transport abstraction
└── cache/ ← cache client wrapper
Domain modules depend on interfaces, not on infra/ directly. Wiring happens at the app root.
Defining the public interface
Every module should have exactly one entry point for external consumers:
TypeScript/JavaScript — barrel file:
// orders/index.ts — only export what callers should use
export { OrderService } from './order.service';
export type { Order, OrderStatus } from './order.model';
// OrderRepository is NOT exported — it's an internal implementation detail
Python — __init__.py:
# orders/__init__.py
from .service import OrderService
from .models import Order, OrderStatus
# Do NOT expose Repository, internal helpers, etc.
Go — package-level exports:
// Unexported identifiers (lowercase) are the boundary mechanism.
// Only export what external packages need.
type OrderService struct { ... } // exported
type orderRepo struct { ... } // unexported — stays internal
Rule: If a caller has to import from a subpath (orders/internal/repo), your
public interface is leaking. Fix the barrel, not the caller.
Step 4 — Break Circular Dependencies
Circular imports are always a design smell. Three techniques:
A. Extract the shared type to a neutral module
Before: auth → user (for User type), user → auth (for AuthToken type)
After: auth → shared/types, user → shared/types (no cycle)
B. Invert the dependency (dependency inversion)
Before: orders → notifications (orders directly calls notification service)
After: orders emits an event; notifications subscribes to it
orders has zero knowledge of notifications
C. Merge if the cycle reveals true cohesion
Before: orderItem ↔ order (they import each other constantly)
After: merge into a single orders module — the cycle was telling you they belong together
Do not resolve cycles by adding a third "glue" module that imports both — this just moves the coupling.
Step 5 — Write the Refactored Structure
Output:
-
The proposed directory/file structure — show the before and after tree.
-
The public interface for each new module — show the
index.ts/__init__.pyexports. -
The migration path — in what order to make the changes to avoid breaking the build mid-refactor:
- Create new module shells first
- Move types before logic (types have fewer dependencies)
- Update imports from outermost callers inward
- Delete old paths last
-
Any behavior questions — flag cases where it's unclear which module should own a piece of logic.
Step 6 — Verify and Annotate
After writing the structure, include:
Boundary health summary:
| Module | Before | After | Change |
|---|---|---|---|
utils.ts (340 lines) |
22 unrelated exports | Deleted — redistributed to orders/, users/, shared/types/ |
Eliminated dumping ground |
orders.ts (480 lines) |
Mixed DB + logic + formatting | Split into order.service, order.repo, order.formatter |
Single concern per file |
Circular: auth ↔ user |
Cycle via shared types | shared/types/auth.ts extracted |
Cycle broken |
Then call out:
- Ownership decisions that were ambiguous — explain the reasoning
- What the public interface deliberately excludes — and why
- Any logic that has no clear owner — surface it rather than guess
Language-Specific Notes
TypeScript / JavaScript
- Enforce barrel imports with ESLint:
no-restricted-importsto ban deep-path imports from other feature modules. pathsintsconfig.jsonfor clean import aliases:@orders/*instead of../../../orders/."exports"field inpackage.jsonfor true encapsulation in monorepos (blocks subpath imports entirely).
Python
__all__in__init__.pyto explicitly declare the public surface.- Use relative imports (
from .service import OrderService) within a package; absolute imports across packages. src/layout (src/myapp/) prevents accidental imports of the package directory itself.
Java
- Package-private (no modifier) for internal classes — only
publicfor the intended interface. - Module system (
module-info.java, Java 9+) for hard boundaries:exports com.app.ordersexposes only what's declared.
Go
- The package is the boundary — internal packages (
internal/) are enforced by the compiler. - Keep package names single-word, lowercase, and domain-describing (
orders, notorderManagement).
Anti-Patterns to Avoid
| Anti-pattern | Why it's bad | Fix |
|---|---|---|
Splitting by type (models/, services/, repos/ at top level) |
Forces you to touch 4 folders for any single feature change | Split by feature (vertical slices) |
A shared/ module that keeps growing |
Becomes the new dumping ground | Shared kernel must be minimal and frozen; push new things to feature modules |
| Splitting a module before understanding its seams | Creates artificial boundaries that don't reflect the domain | Map responsibilities first; let the seams emerge |
| Public interface that exports everything | Defeats the purpose of the boundary | Default to not exporting; add exports only when a caller needs them |
| Resolving a cycle by making both modules import a third "bridge" | Adds indirection without removing coupling | Extract the shared type, not the shared logic |
Quick Checklist Before Delivering
- Each module has a one-sentence responsibility statement with no "and"
- Every module has a single entry-point barrel/init that is the only import path for external consumers
- No circular dependencies remain
-
utils/helpers/commondumping grounds have been redistributed or scoped - Cross-layer imports (UI→DB, domain→HTTP) eliminated via boundary types
- Migration path is sequenced to avoid mid-refactor breakage
- Ambiguous ownership decisions surfaced and explained
- Before/after structure tree included
More from blunotech-dev/agents
anti-purple-ui
Enforce a strict monochrome UI with a single high-contrast accent color, removing generic tech gradients and “AI-style” palettes. Use when the user wants minimal, anti-AI, or non-generic aesthetics, or says the UI looks too techy or generic.
9harmonize-whitespace
Align all spacing (padding, margins, gaps) to a consistent 4pt/8pt grid. Use when spacing feels off, inconsistent, cramped, or unbalanced, or when the user asks for a spacing scale or alignment fix.
9typographic-hierarchy
Improve typography by adjusting font sizes, weights, spacing, and contrast to create clear visual hierarchy and readability. Use when text feels flat, unstructured, or when the user asks to refine headings, type scale, or overall readability.
6micro-interaction-adder
Add polished CSS micro-interactions like hover effects, transitions, and feedback states to improve UI feel. Use when the user asks for animations, better UX, or when the interface feels static, plain, or unresponsive.
4consistent-border-radius
Normalizes rounded corners across a file so buttons, inputs, cards, modals, badges, and all UI elements share the exact same curvature. Use this skill whenever the user mentions inconsistent border radii, wants to unify rounded corners, asks to make UI elements look more cohesive, or says things like "make the corners match", "fix the rounding", "unify border radius", "standardize my rounded corners", or "buttons and cards don't match". Also trigger when the user pastes a CSS/HTML/JSX/TSX file and asks for a design consistency pass, border radius is one of the first things to normalize.
4component-split
Analyze a component and determine when and how to split it based on size, responsibility, and reuse signals, producing a refactored structure with clear boundaries. Use when users share large, mixed-concern, or hard-to-test components, or ask about splitting, refactoring, or improving component architecture.
3