JavaScript Program Architecture
Design maintainable JavaScript programs by defining module responsibilities, public APIs, dependency direction, state ownership, side-effect boundaries, lifecycle, and composition roots.
Intermediate
Architecture is about boundaries, not folder names
JavaScript program architecture is the deliberate arrangement of responsibilities, dependencies, state, and side effects. A useful architecture makes it obvious which module owns a decision, which module is allowed to depend on it, and where input or external effects enter the system. Folder names alone do not create those boundaries.
Output
Ready to run.
Quick check
Which boundary is most useful when designing a module?
Give each module one coherent responsibility
A module does not need to contain one function, but its exported surface should describe a coherent responsibility. Mixing parsing, domain rules, persistence, and presentation in one module makes every change cross unrelated concerns.
Output
Ready to run.
Quick check
Which design keeps a domain calculation easier to reuse?
Design a small public API
A module boundary is also an API boundary. Export only the operations other modules need. Keep implementation helpers private so callers depend on stable concepts rather than incidental details.
Output
Ready to run.
Quick check
What is the architectural reason to keep normalize() private?
Make dependency direction visible
A healthy dependency graph has an intentional direction. Higher-level orchestration can depend on domain operations, while low-level adapters can implement infrastructure contracts. When a low-level detail reaches upward into application policy, the dependency direction becomes harder to change.
Output
Ready to run.
Quick check
Which dependency direction is easier to change?
Separate pure core logic from side effects
Pure calculations are easiest to test and reuse when they receive inputs and return outputs. Side effects such as logging, DOM mutation, storage, or network access should live at explicit boundaries around that core.
Output
Ready to run.
Quick check
Where should a DOM mutation normally live?
Choose one owner for mutable state
Shared mutable state becomes difficult when multiple modules can mutate it without a clear owner. Prefer one module that owns the state transitions and exposes operations or read-only views to consumers.
Output
Ready to run.
Quick check
Why define one owner for mutable state?
Use dependency injection when a detail should be replaceable
If an application needs a replaceable clock, storage service, logger, or network client, pass that dependency into the operation that needs it. This keeps the operation independent from one concrete implementation and creates a clear testing seam.
Output
Ready to run.
Quick check
What does passing clock into makeGreeting provide?
Put composition in a deliberate entry point
The entry point is a useful place to assemble concrete modules: choose adapters, connect dependencies, create state owners, and start the application. This keeps construction decisions from leaking into reusable domain modules.
Output
Ready to run.
Quick check
What belongs naturally in a composition root?
Keep infrastructure behind an adapter boundary
Adapters translate an external mechanism into a stable application-facing contract. The rest of the application can depend on that contract instead of importing browser storage, a particular HTTP client, or a database-specific detail everywhere.
Output
Ready to run.
Quick check
What is the adapter hiding?
Avoid cycles by extracting shared concepts
Circular dependencies can be valid ECMAScript module graphs, but an architectural cycle can also signal that two features are coupled at the wrong level. When two modules need a shared concept, extract that concept into a lower-level module instead of making each feature depend directly on the other.
Output
Ready to run.
Quick check
What is a useful response when two feature modules depend on each other only for a shared constant?
Make initialization and teardown explicit
Programs with subscriptions, timers, event listeners, workers, or other resources need lifecycle boundaries. An architecture should make it possible to start a feature and later release what it acquired, rather than hiding setup in module evaluation with no corresponding cleanup path.
Output
Ready to run.
Quick check
Why should a long-lived feature expose cleanup?
Treat side effects as a visible architectural choice
Importing a module can execute its top-level code, so side effects should be deliberate. Prefer modules whose exports describe capabilities, and isolate unavoidable startup effects in clearly named entry points or initialization functions.
Output
Ready to run.
Quick check
Which import design makes startup behavior easiest to reason about?
Architect for testing without designing fake production code
A good testing seam follows a real architectural boundary. If a module accepts a clock, repository, parser, or logger as a dependency because production genuinely needs that abstraction, tests can supply a deterministic implementation without changing the production design.
Output
Ready to run.
Quick check
What makes this a useful testing seam?
Scale features by composing stable boundaries
As an application grows, organize around responsibilities and dependency boundaries rather than forcing every feature into one universal architecture. A feature can expose a small API, own its state, use adapters for infrastructure, and be composed by an entry point.
Output
Ready to run.
Quick check
What makes this feature composition scalable?
Use an architecture review checklist before adding another module
Before creating a module, ask: What responsibility does it own? What should callers know? Which dependencies does it need? Which state does it own? Where do side effects begin and end? Can the dependency direction remain clear? What lifecycle does it have? These questions are more durable than a particular folder convention.
What does this module own?
What is its public API?
Which direction do its dependencies point?
Who owns mutable state?
Where are side effects?
How does it start and stop?Quick check
Which question should come first when adding a module?
Authoring rules
Keep module APIs small and intentional. Make dependency direction visible. Keep domain rules independent from replaceable infrastructure when practical. Give mutable state a clear owner. Put composition decisions in an entry point. Make long-lived side effects and cleanup explicit. Use dependency injection for genuine replaceable boundaries, not as ceremony. Treat circular dependencies as a signal to inspect ownership, not as an automatic syntax error.
Practice: refactor a tangled feature
Take a small application that mixes state, rendering, persistence, and business rules in one module. Identify the domain decision, state owner, infrastructure boundary, and composition root. Extract only the boundaries that clarify responsibilities; do not split code merely to increase the module count.
Specification connection
ECMAScript defines modules, module environments, module records, linking, evaluation, and module namespace objects. Program architecture is a learner-facing design layer built on those language mechanisms; it is not itself an ECMAScript specification feature. The architecture rules in this lesson are therefore presented as engineering guidance, while the module mechanics remain grounded in ECMA-262.
ModuleRecord → dependency loading → linking → evaluation
Architecture adds deliberate boundaries around those language mechanisms.Thanks for your feedback.
Mark this learning resource complete to track your learning progress.