Modern Angular
Standalone Without Turning NgModules Into a Villain
Standalone changes the architectural default, but it does not make every existing NgModule an emergency. Components are standalone by default since Angular v17 and NgModule has been optional since v19. The official standalone migration docs are explicit: existing apps can adopt incrementally without breaking changes.
The better question
In a mature Angular app, the standalone conversation should not start with 'how do we remove every module?'. Start with a smaller question: where does this feature's boundary live today, and would standalone make that boundary easier to read, load, and test?
Angular components are standalone by default in current Angular. That changes the default for new code. It does not mean a stable internal library, a third-party integration, or a legacy feature module must be rewritten before the next release.
I would keep a stable LegacyPaymentsModule if it wraps a third-party SDK, exposes a known public API, and is not blocking lazy loading, ownership, or tests. That is not nostalgia; it is scope control.
The decision table
| Situation | Default move | Evidence to require |
|---|---|---|
| New route or feature | Use standalone components and route-level providers | Imports are explicit and the route can own the feature facade. |
| Module that only exists for declarations | Migrate when the area is touched | Build and feature smoke test pass without broad import churn. |
| SharedModule with everything | Split by real usage | A dependency list shows which imports each feature actually needs. |
| Stable library module | Keep it unless there is a release reason | The module still expresses a public API or third-party contract. |
| Third-party SDK wrapper | Keep or isolate behind a route | The migration would add risk without improving ownership. |
| Root providers everywhere | Move feature state closer to the route | Navigation, reuse, and teardown behavior are understood. |
A route is a better boundary than a slogan
The useful standalone migration is often a route migration. A route can lazy-load a component, scope providers, and become the place where the team sees the feature boundary without opening a module file first.
If every provider stays at root and every component imports a giant shared bucket, the code is not meaningfully more modular. It only removed a layer of ceremony.
export const routes: Routes = [
{
path: 'billing',
providers: [BillingFacade],
loadComponent: () =>
import('./billing/billing-page.component')
.then(m => m.BillingPageComponent),
},
];What to avoid
Avoid a migration branch whose only promise is 'remove NgModules'. That kind of branch tends to touch too many files, create review fatigue, and produce little product value.
The better move is narrower: use standalone as the default for new code, migrate modules that block lazy loading or hide dependencies, and leave stable modules alone until the team has a reason to touch them.
Reusable artifact
Standalone adoption rule
- Use standalone by default in new Angular code.
- Use routes as feature boundaries, not SharedModule as the first design tool.
- Keep NgModules when they still express a stable integration boundary.
- Migrate touched modules only when the diff improves ownership, loading, or reviewability.
Sources checked
- https://angular.dev/guide/components
- https://angular.dev/reference/migrations/standalone
- https://angular.dev/guide/di/dependency-injection-providers
- https://angular.dev/api/router/Route
- https://angular.dev/api/core/inject
- https://blog.angular.dev/announcing-angular-v21-57946c34f14b
Modern Angular Playbook
This article is one play.
The Modern Angular Playbook collects the diagnostic, the adoption matrix, eleven plays, and the 30-day plan. Free, in English and Portuguese.
You get both PDFs by email, through the Dojo IA list.
André Ramosavailable for remote roles, UTC−3Get in touch →