Introduction
Most Angular applications don't collapse because of bad components — they degrade because of architectural entropy. I've seen well-written components sitting inside systems that are fundamentally unscalable: tightly coupled features, services acting as dumping grounds, state scattered across the app, and onboarding that feels like reverse-engineering a legacy system. The code itself is fine. The architectural intent — the deliberate choices about boundaries, ownership, and how complexity grows — was never established.
Angular gives you enough flexibility to either build a clean, evolvable system or a slow-moving monolith disguised as a SPA. The difference is not syntax or tooling. It's whether the team made conscious structural decisions early, or let the framework's permissiveness make those decisions by default.This article is a set of architectural decisions I've found consistently reduce complexity while keeping systems adaptable as they scale — along with the signals that tell you when each one becomes necessary.
1. Feature-Based Modular Architecture (The Foundation)
The most important decision you make early is how you structure your codebase. A type-based structure (components/, services/, models/) looks clean initially, but it doesn't scale with teams or domains. It fragments ownership and creates implicit coupling across the app: a change to a service in services/ ripples unpredictably because nothing constrains who depends on it.
A feature-based structure aligns code with business domains:
/features
/orders
/ui
/data-access
/state
/users
/ui
/data-access
/state
This is not just folder organization — it's a boundary definition. Each feature owns its UI, its data access layer, and its state. The critical addition is a public API boundary via index.ts. Nothing outside the feature should reach into its internals. The anti-pattern this prevents is the SharedModule dumping ground — a module that accumulates everything "used in more than one place" until it becomes an implicit dependency for the entire app. If you find yourself regularly adding unrelated things to a shared module, that's the signal that you need feature boundaries. Once SharedModule becomes load-bearing infrastructure, untangling it costs significant refactor effort.
When to introduce it: From day one on any app that will grow beyond a single developer, or that maps to more than one business domain. The upfront cost of structure is always cheaper than the retroactive cost of introducing it after coupling has set in.
2. Smart vs. Dumb Components (Still Relevant, but Evolved)
The classic container/presenter split still holds — but not in the rigid way it was originally taught. Smart components orchestrate data and interactions. Dumb (presentational) components focus purely on rendering, accepting inputs and emitting outputs. The value of the split is not organizational tidiness — it's testability and reusability. A component that fetches its own data and manages its own side effects cannot be reused in a different context without dragging all of that logic with it. What's changed is Angular's evolution with signals. The original reason deep container hierarchies existed was to push state down through the component tree via prop-drilling, with a smart ancestor fetching data and passing it downward across multiple levels. Signals eliminate much of that: reactive state can be consumed directly at the component level without a container managing the distribution chain. The architectural implication is that you need fewer layers of orchestration, not that the separation of concerns disappears.
I treat this pattern as a spectrum, not a rule. For reusable UI elements, keep them pure and stateless — no async operations, no service injection. For feature entry points, allow orchestration. The failure mode to avoid is the middle ground: components that are almost dumb but hold just enough state or logic to be untestable in isolation.
When to introduce the split: When you find yourself copying and pasting a component into a second context and realizing you have to strip out its data-fetching logic to make it work there. That duplication pressure is the signal that presentation and orchestration need to be separated.
3. State Management Is a Strategy, Not a Library Choice
One of the most common architectural mistakes is prematurely committing to a state management library. Not every app needs NgRx. In fact, most don't — at least not initially. The right question is not "which library should we use" but "how complex is our state, and what does that complexity actually demand?" Think in terms of state complexity, not tooling. For small apps, signals or component-level state with minimal abstraction give you the fastest iteration cycle. For mid-scale, state encapsulated inside injectable services provides a clean balance between structure and simplicity — services become the single source of truth for a domain without requiring action/reducer/effect boilerplate. For large systems dealing with cross-feature coordination, complex async side effects, or strict debugging requirements, structured state via NgRx or ComponentStore becomes justified by the operational demands, not by preference.
Angular Signals have shifted this conversation significantly. Many patterns previously requiring RxJS pipelines of meaningful complexity — derived state, reactive UI updates, coordinated async flows — can now be expressed more declaratively with computed signals. This doesn't deprecate RxJS; it gives you a simpler tool for cases where RxJS was being used defensively rather than because the problem genuinely required it. The anti-pattern here is global state for trivial data — routing a form's input value through a store because the store already exists. The cognitive overhead of following an action through reducers and selectors for data that only one component cares about is pure friction.
When to introduce structured state: When you observe two or more features needing to coordinate around shared state, or when debugging async interactions becomes difficult because you can't inspect what happened and in what order. Until those conditions exist, service-based state is sufficient.
4. The Facade Pattern (The Most Underrated Pattern in Angular)
If there's one pattern that consistently improves maintainability in large Angular systems, it's the facade. A facade sits between your UI layer and your underlying implementation — whether that's a store, a direct API call, or a signals-based state container. Instead of components depending directly on store selectors and dispatch calls, they depend on a facade that exposes a stable interface.
Instead of:
Component → Store → Effects → API
You get:
Component → Facade → (Store / API / Signals)
The architectural benefit is decoupling of interfaces from implementations. When you decide to migrate from a service-based state approach to NgRx — or from NgRx to signals — you change the facade internals without touching the UI layer. Components remain stable across the migration. This also makes testing significantly simpler: mock the facade, not the store. A mocked facade is a few stub methods. A mocked NgRx store requires substantially more setup.
When to introduce it: When you have more than one component depending on the same state or API surface, or when you anticipate a state management migration. The cost of retrofitting facades into an existing app is much higher than introducing them as you build features — once components are tightly bound to store primitives, extracting that dependency touches every consumer.
5. Standalone + Functional Architecture (Modern Angular Direction)
Angular's move toward standalone components is not just syntactic — it's architectural. Removing NgModules reduces indirection and makes dependencies explicit. When a component declares its own imports, you can read what it depends on without navigating to a module file to infer it. This makes large codebases significantly easier to reason about during code review and refactoring. The accompanying shifts — functional guards, functional resolvers, composable routing — align Angular closer to a composition-first model where behavior is built from small, explicit, independently testable units rather than assembled through module wiring. The practical consequences are real: faster onboarding because new developers can read a component file and understand its dependencies without mental model construction; better tree-shaking because unused imports are no longer masked by module re-exports; clearer dependency graphs that static analysis tools can actually travers
When to migrate: For greenfield projects, start standalone by default. For existing NgModule-based codebases, the signal is onboarding friction — when new team members consistently struggle to understand the module graph, the indirection is costing more than it's providing.
6. Data Access Layer Isolation (Where Most Systems Break)
A subtle but critical boundary is how you handle API interaction. The common anti-pattern is components directly calling HTTP services and transforming responses inline. The consequence is that business logic leaks into the UI layer. When the API contract changes — and it always does — those changes ripple through components rather than being absorbed at the boundary. A more scalable approach gives each feature its own data-access layer: API interaction is centralized and typed, error handling and response transformation happen before data reaches the UI, and components receive view models rather than raw API responses.
This is also where the god service anti-pattern tends to emerge: one injectable that handles authentication, user preferences, notification state, and API calls for three different domains because it started as a utility and accumulated responsibility over time. A god service is a symptom of the absence of feature boundaries — it exists because there was no designated owner for each domain's data access concerns.
When to enforce it: When you find yourself writing HTTP calls in component lifecycle hooks, or when a change to a backend contract requires modifying multiple components. The former is a design smell; the latter is evidence that the transformation layer is missing.
7. Monorepo + Library Architecture (When Teams Scale)
At a certain point, the problem is no longer code — it's coordination. Multiple teams working in the same codebase create a specific class of architectural failure: implicit cross-feature coupling that no one intended but everyone propagates because there are no enforced boundaries. This is where tools like Nx become relevant. A monorepo with library architecture allows you to enforce explicit dependency direction — features cannot import from each other arbitrarily, shared libraries remain stable and minimal, and architectural constraints are enforced via linting rather than convention.
The full mechanics of planning and executing a monorepo migration — dependency graph analysis, CI/CD restructuring, team adoption strategy — are substantial enough to deserve their own treatment. I cover this in detail in a dedicated piece on migrating from multi-repo to monorepo architecture.
When to introduce it: When cross-team imports are happening without review gates, or when a change in one area of the app causes unexpected test failures in an unrelated area. Those signals indicate that boundaries exist conceptually but not structurally.
8. A Practical Decision Framework
If I were starting a new Angular project today, the architecture would evolve like this:
Start with standalone components, a feature-based structure, and signals for local state. These three decisions establish the foundation with low ceremony and maximum legibility. As complexity grows, introduce service-based state and add facades to decouple UI from state implementation. At scale, graduate to structured state with NgRx or ComponentStore, and move toward monorepo architecture with Nx when team coordination becomes the bottleneck Throughout all phases: enforce feature boundaries, refactor toward domain ownership, and resist premature abstraction. The most expensive architectural mistakes are not the patterns you chose — they're the patterns you introduced before the system demanded them, and then couldn't remove.
Summary
Good architecture does not prevent entropy — it controls the rate at which it accumulates. Every system accrues complexity over time. The question is whether that complexity is incidental — the consequence of decisions made too fast, without architectural intent — or structural, absorbed deliberately into boundaries that were designed to contain it.
In Angular, the teams that build systems that last are not the ones using the most patterns. They're the ones who understood when each pattern earned its place, introduced it at the right time, and resisted the rest.
Top comments (0)