A frontend team can spend an entire meeting deciding whether a component belongs in components/, features/, or modules/ without making the system easier to change.
The folders look cleaner afterward. The checkout page still imports pricing rules from the catalog. Three teams still edit the same customer model. A small product change still requires coordinating several releases.
Folder structure makes architecture visible. It does not create the boundaries that architecture needs.
Start with a change, not a directory
Consider a storefront with catalog, cart, and checkout capabilities. Suppose checkout needs to support a new delivery option.
A useful architectural question is: how far should that change travel?
Ideally, checkout owns the delivery selection flow and its presentation. It consumes a shipping quote through an explicit contract. It should not need to understand how the catalog renders product cards or how the cart stores its internal state.
If implementing that option requires editing all three features, investigate why. Some changes legitimately cross boundaries. Others cross them because one feature has quietly become another feature's implementation detail.
Architecture is the set of decisions that determines that difference.
A boundary needs rules
A folder named checkout is just a folder if any part of the application can import anything inside it.
A useful module boundary defines:
- What the module owns: behavior, state, and decisions.
- What it exposes: a small public interface.
- What it depends on: explicit contracts with other modules.
- Who maintains it: a team or person responsible for its evolution.
For example:
// checkout/index.ts: public entry point export { CheckoutPage } from './ui/CheckoutPage'; export type { ShippingQuote } from './contracts'; Consumers import from this entry point. They do not reach into checkout/state/internalStore because an implementation happens to be convenient.
The entry point alone does not enforce anything. Import restrictions in linting or dependency checks can make the rule executable. Without enforcement, a boundary depends on everyone remembering it under deadline pressure.
Measure coupling through coordination
Coupling is more than an import count.
Two modules can communicate through a single function and still be tightly coupled if they must always change together. Conversely, several imports from a stable, narrow interface may create little coordination cost.
Look for recurring patterns:
- One feature's internal refactor breaks another feature's tests.
- Teams need synchronized changes to ship ordinary work.
- A shared object accumulates fields needed by unrelated consumers.
- Developers must understand a large part of the application before making a local change.
These signals reveal where knowledge is leaking across boundaries.
A dependency diagram helps, but change history adds context. Files that repeatedly change together can reveal a missing boundary or an existing boundary drawn in the wrong place. Treat that as evidence to investigate, not an automatic instruction to merge them.
Ownership must include decision authority
Assigning a team to a directory is insufficient if that team cannot change its public interface, prioritize maintenance, or reject inappropriate dependencies.
Ownership means being responsible for outcomes and having authority over the decisions that produce them.
For a checkout module, that might include its interaction design, validation behavior, and client state. Payment authorization remains a server responsibility. The frontend boundary should make that distinction clear instead of implying that all checkout rules belong in browser code.
A short ownership note often helps more than another directory level: purpose, public interface, allowed dependencies, and maintainer.
Refactor one dependency at a time
You do not need to reorganize the whole repository.
Choose a feature with frequent changes. Trace its dependencies. Find one place where another module imports its internals. Introduce a contract that expresses what the consumer actually needs, then move the dependency behind it.
Sometimes the right fix is moving behavior. Sometimes it is duplicating a small piece of presentation. Sometimes it is accepting a dependency because the concepts really do belong together.
The goal is predictable change, not the smallest number of arrows.
A good folder structure helps developers locate code. A good architecture helps them know what they can change, what they must preserve, and who needs to be involved. Judge the architecture by those outcomes.
