Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

JavaScript Program Architecture

Design maintainable JavaScript programs by defining module responsibilities, public APIs, dependency direction, state ownership, side-effect boundaries, lifecycle, and composition roots.

Levels: Intermediate ~45 min read

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

javascript
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.

text
ModuleRecord → dependency loading → linking → evaluation

Architecture adds deliberate boundaries around those language mechanisms.
Was this page helpful?

Mark this learning resource complete to track your learning progress.