DEV Community

kirandeepjassal-crypto
kirandeepjassal-crypto

Posted on Originally published at prepstack.co.in

Enterprise Angular Architecture in 2026 — Core, Shared, Feature + Nx Monorepo (Real Project Layout, Production Metrics)

Most Angular apps don't fail because the framework runs out of steam. They fail around the 18-month mark, when the directory tree turns into a swamp: shared/utils/helpers/index.ts re-exports everything, three teams import each other's internals through it, CI takes 14 minutes because one test change rebuilds the world, and nobody can ship a feature without breaking two others.

The four patterns here — Feature, Core, Shared, and an Nx monorepo with enforced library boundaries — are what stops that. Not 2017 NgModule dogma; the directory-and-dependency-graph discipline underneath, which works just as cleanly with Angular 19 standalone components.

Every section uses the same real app: Mattrx, a multi-tenant marketing-analytics SaaS — Angular 19 standalone, 4 apps in one repo, 540+ components, 22k LOC, 5 product teams sharing a design system and data-access layer.

The mental model: the dependency graph IS the architecture

The single biggest decision in a large Angular app is what's allowed to import what. The arrows go ONE way:

Apps (shells) -> Features (lazy) -> Shared (ui, util)
                     |
                     v
                   Core (imported only by the app shell)

Never:
  Shared -> Features    (shared can't know about features)
  Features -> Features  (features can't reach across each other)
  Anything -> Core, except the app shell
Enter fullscreen mode Exit fullscreen mode
Layer Owns Imported by
Core auth, interceptors, error handler, config, logger only main.ts, once
Shared buttons, tables, pipes, DTOs - no business logic any feature
Feature routes, components, services, state for one capability the app router (lazy)
Nx libs tagged boundaries, ESLint enforcement, nx affected the whole repo

Core — the app-singleton layer (modern, no NgModule)

export function provideCore(): EnvironmentProviders {
  return makeEnvironmentProviders([
    AuthService, ConfigService, LoggerService,
    { provide: ErrorHandler, useClass: GlobalErrorHandler },
    provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
    provideAppInitializer(() => inject(ConfigService).load()),
  ]);
}

// apps/customer/src/main.ts — called exactly once
bootstrapApplication(AppComponent, {
  providers: [provideCore(), provideRouter(routes), provideAnimationsAsync()],
});
Enter fullscreen mode Exit fullscreen mode

makeEnvironmentProviders is the modern equivalent of the "Core module can only be imported once" guard.

Shared — the cardinal rule

Shared cannot import from Features. Ever. If a "shared" component needs to know about campaigns, it isn't shared — it's a campaigns component.

Each Shared component is its own Nx library, imported at the leaf — no barrel @mattrx/shared/ui that re-exports everything (so the bundler tree-shakes per-library, not per-monolith):

import { MxButton } from '@mattrx/shared/ui/button';import { MxTable } from '@mattrx/shared/ui/table';```

Don't ship a `SharedModule` in 2026 — importing it to use `<mx-button>` drags in `<mx-modal>`, `<mx-toast>`, `<mx-table>`, and everything else. Use standalone leaves.

## Feature — one bounded business capability, lazy-loaded



```ts
// app.routes.ts — the app router doesn't know the feature's internal structure
{ path: 'campaigns', canMatch: [authGuard],
  loadChildren: () => import('@mattrx/features/campaigns').then(m => m.CAMPAIGNS_ROUTES) },
Enter fullscreen mode Exit fullscreen mode

The bundler emits one chunk per feature. Landing on /dashboard downloads main.js (~280 KB) + dashboard.chunk.js (~140 KB) — not campaigns, inbox, reports. Feature state is Signals-first (toSignal at the HTTP boundary, computed for derived).

Nx — the structural firewall

Tags declare what each library is and belongs to, and ESLint enforces the arrows:

"@nx/enforce-module-boundaries": ["error", { "depConstraints": [
  { "sourceTag": "type:feature",     "onlyDependOnLibsWithTags": ["type:ui","type:data-access","type:util"] },
  { "sourceTag": "type:ui",          "onlyDependOnLibsWithTags": ["type:ui","type:util"] },
  { "sourceTag": "type:util",        "onlyDependOnLibsWithTags": ["type:util"] },
  { "sourceTag": "scope:customer",   "onlyDependOnLibsWithTags": ["scope:customer","scope:shared"] }
]}]
Enter fullscreen mode Exit fullscreen mode

A type:ui library that imports a type:feature library is an ESLint error in CI. That's the difference between "we have a convention" and "the codebase enforces the convention."

And nx affected runs CI only for what the PR touched:

- run: npx nx affected --target=build --base=origin/main   # 2.5 min, not 12
Enter fullscreen mode Exit fullscreen mode

The before/after (after the structure landed, 4 weeks)

Metric Before After
CI on a typical PR 22 min 6 min
affected:build 12 min 2.5 min
affected:test 8 min 90 s
Initial JS (gzipped) 1.2 MB 290 KB
Cross-team merge conflicts/wk ~6 ~1
Scaffold a new feature ~3 days ~4 hours
"Where does this go?" PR comments/wk ~12 ~1
Velocity (features/sprint) 9 14

The migration path (4 weeks, not one big PR)

Week 1 — tease apart Core (move Auth, interceptors, error handler, logger, config; replace CoreModule with provideCore()). Week 2 — tease apart Shared (one lib per component, tag everything, turn on @nx/enforce-module-boundaries, fix violations). Week 3 — extract Features one at a time, most isolated first, ~1-2/day. Week 4 — enable nx affected in CI + add any second frontend as a new app that reuses your shared libs.

The right mental model

Enterprise Angular at scale isn't NgModules vs standalone — it's the dependency graph being a layered DAG, and the codebase enforcing that picture so humans don't have to. Core = app-singletons imported once. Shared = presentation + utilities that know no business concepts. Features = lazy-loaded leaves that can't reach each other. Nx = makes the graph visible, enforces it, makes CI fast.

Three habits: generate every library (nx g) with tags from day one; treat boundary violations as test failures; keep apps as thin shells.

The full guide has the real Mattrx layout with file counts, the classic-vs-standalone code for every layer, the nx graph, the full ESLint config, the decision tree for "where does this go?", and the week-by-week migration path:

https://prepstack.co.in/blog/enterprise-angular-architecture-feature-core-shared-modules-nx-monorepo-guide

Originally published on PrepStack.

Top comments (0)