All articles & videos

Frontend architecture for enterprise: draw the boundaries first

Enterprise frontend architecture is boundaries: layers with a may-import contract, enforced in lint, or not, mostly not.

Adil Ben Moussa
08 November 2026· 5 min
Frontend architecture for enterprise: draw the boundaries first

A imports a store "just this once". Five years later, half the ui folder knows about the backend, and nobody can restyle a button without a regression in transfers. That is how enterprise frontends age: not through one bad decision, but through a boundary nobody enforced.

Architecture is the part that outlives the framework, so it comes first in this series. The question is not which pattern you pick. It is which lines your teams may not cross, and what stops them.

Layers with a "may import" contract

Split the code by type, not only by feature: domain (pure models and rules), data-access (the only code that talks to the backend), ui (inputs and outputs only), feature (wires the three), and app (shell and routes). Then write down who may import whom.

With one team this feels like ceremony. From the second team on it is the only thing that keeps their work apart. The useful rule is short: ui never reaches data-access, domain imports nothing, and nothing modern imports the legacy code.

A rule in a wiki erodes. Put it in lint.

lets you tag every project and say which tags may depend on which. Its own docs give the reason in one line: "If all of them can depend on each other freely, chaos will ensue, and the workspace will become unmanageable." Not on Nx? "enforces module boundaries and dependency rules in TypeScript", so it works in a plain CLI project too.

The lint error is the point. A reviewer gets tired on a Friday afternoon. The rule does not.

Monorepo or polyrepo

Nx puts it plainly: with modern tooling "the choice is mostly organizational". The same page advises a "when strict repository-level access control is a hard requirement, or when teams share little code".

In enterprise I would turn that around into a test. Do your teams share a , a domain model and one ? Then one repository, with boundaries inside it. Do separate products ship on their own schedules and share almost nothing? Then separate repositories. A monorepo also does not force one release: projects build from the same commit, but "teams control when a tag reaches production".

Micro-frontends: mostly not

Micro-frontends are "independently deliverable frontend applications" composed into one product. The win is real: each team deploys on its own. The cost is real too. The same article warns that "independently-built JavaScript bundles can cause duplication of common dependencies", and asks whether you have "the technical and organisational maturity required".

My test: choose them when separate organisations must deploy independently into one shell. Do not choose them to fix a messy . A monolith without boundaries becomes five repositories without boundaries, plus a runtime contract.

Micro-frontends do not fix a missing boundary. They move it to runtime.

The design system and the legacy corner

The design system is a library like any other, and its tokens are the contract. Components read colour by role, not by hex, so a rebrand is one file and not four hundred.

The legacy corner gets the opposite treatment: frozen, wrapped, and imported by nobody new. In a hybrid app, "each DOM element on the page is owned exactly by one framework", and the import graph should say the same thing. The arrow points one way, from old to new, so the old code stays deletable.

Record every crossing

Sometimes a boundary must move. That is fine, once it is written down. An "captures a single AD and its rationale", and the next team will need the rationale more than the code.

On Atlas Bank

Atlas Bank, the demo app of this series, encodes the contract in eslint.config.mjs. Here are the ui, feature and legacy lines:

  {
    sourceTag: 'type:ui',
    onlyDependOnLibsWithTags: ['type:ui', 'type:domain'],
  },
  {
    sourceTag: 'type:feature',
    onlyDependOnLibsWithTags: [
      'type:feature',
      'type:ui',
      'type:data-access',
      'type:domain',
    ],
  },
  {
    sourceTag: 'type:legacy',
    onlyDependOnLibsWithTags: [
      'type:legacy',
      'type:data-access',
      'type:domain',
    ],
  },

npm run lint:arch fails when ui imports a store. sheriff.config.ts says the same in Sheriff for teams without Nx. The legacy NgModule screen in libs/legacy/transfers may read the modern stores, and no modern library may import it. The repo's AGENTS.md adds the human half: "Crossing a library boundary needs an ADR link in the PR." Colours live in apps/atlas/src/styles/tokens, and npm run check:tokens fails on a raw colour in any other stylesheet (the frozen legacy corner keeps its old look until it is deleted).

What I left out, and why

  • Clean or as labels. The layers above are that shape. The name does not change a decision.
  • details. Tooling that changes every year. Decide on micro-frontends first; the tool follows.
  • Monolith versus . A backend debate.
  • . Code level, not system level.

On Monday

  • Draw your libraries and the arrows between them. Every arrow you did not plan is a candidate rule.
  • Add one rule to lint, the one that hurts most (often ui to data-access). Let it fail, and fix or record each case.
  • Write the first ADR for the crossing you cannot remove yet.

Next: Part 2, on state.

For your coding agent

The rules of this article, written for an agent. Paste them into your AGENTS.md, save them as a skill, or point your agent at the file.

For your coding agent Download .md
# Frontend architecture rules for enterprise apps

Source: https://tt-tech.io/blog/frontend-architecture-enterprise-boundaries
For Angular and React frontends. Add to AGENTS.md or save as a skill. Your project's own rules win where they differ.

## Rules

1. Split code by type, not only by feature: `domain` (pure models and rules), `data-access` (the only code that talks to the backend), `ui` (inputs and outputs only), `feature` (wires the three) and `app` (shell and routes). Put new code in the layer it belongs to.
2. Keep the "may import" contract: `ui` never imports `data-access`, `domain` imports nothing, and nothing modern imports the legacy code.
3. Enforce boundaries in lint, not in a wiki. On Nx, tag every project and set which tags may depend on which (`onlyDependOnLibsWithTags`). Without Nx, use Sheriff.
4. When the boundary lint fails, let it fail: fix the import, or record the crossing. Never switch the rule off to make the change pass.
5. When a boundary must move, write it down in an Architectural Decision Record (ADR) with the rationale, and reference it in the pull request.
6. Treat the design system as a library under the same rules. Components read colour from tokens by role, never from a raw hex value.
7. Keep the legacy corner frozen and wrapped. No new code imports it. Dependencies point one way, from old to new, so the old code stays deletable.
8. Treat monorepo or polyrepo as an organisational choice. One repository with boundaries inside fits teams that share a design system, a domain model and one release train. Separate repositories fit products that ship on their own schedules and share almost nothing, or when strict repository-level access control is a hard requirement.
9. Do not introduce micro-frontends to fix a messy codebase. They fit when separate organisations must deploy independently into one shell. Fix the missing boundary first.

## Before you say done

- Run the project's boundary lint (Nx module boundaries or Sheriff) and fix every new error.
- Search the diff for imports from `ui` into `data-access`, from modern code into legacy code, and for raw colour values in stylesheets.
- List every boundary crossing in the pull request with its ADR.

Sources

  1. Nx, Enforce Module Boundaries, docs
  2. Sheriff, Introduction, docs
  3. Nx, Monorepo vs Polyrepo, docs
  4. Cam Jackson, Micro Frontends, martinfowler.com, 2019
  5. Angular, UpgradeModule, API reference
  6. ADR GitHub organization, Architectural Decision Records (ADRs)
Share this article