How would you sequence an incremental standalone migration of a large module-based Angular app so that every intermediate step ships safely?
answer
- every step must still ship
- providers hygiene before conversion
- leaves first, modules next
- bootstrap switch last
basics
~20 sFix provider hygiene first, write new features standalone, convert leaf declarables and import them into their old modules, delete emptied modules, and switch bootstrapModule to bootstrapApplication last. Interop keeps every step shippable, and stopping early is a legitimate choice.
solid answer
~40 sI would follow the order Angular's own migration assumes, convert declarations, remove unneeded NgModules, then switch bootstrap, and wrap it in risk controls. First move singleton services to `providedIn: 'root'` so routed standalone components cannot duplicate them, and adopt `provideHttpClient`/`provideRouter` inside `AppModule`. New features are standalone from day one. Existing code converts leaf-first per area: each converted class is imported and re-exported by its old module, so consumers do not change. Modules that only re-export are then deleted, and the bootstrap switch comes last, when `AppModule` is just providers. Each step lands separately and is verified by the AOT build and service-identity checks. Stable, untouched areas can stay modules.
code
ts · 16 linesimport { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { App } from './app/app'; // now standalone
import { routes } from './app/app.routes';
const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(withInterceptorsFromDi()),
],
};
bootstrapApplication(App, appConfig).catch((err) => console.error(err));go deeper
Know that migration can be incremental because standalone classes and NgModules can import each other.
Explain the three-step order: convert declarations, remove unnecessary modules, switch bootstrap, and why each intermediate state still compiles.
Add the risk controls: provider hygiene first, leaf-first conversion per area, AOT build and service-identity checks after each step.
Own the tradeoffs: pace versus review load, coexistence period, published library surfaces, and when stopping short of full conversion is the right call.
## The question behind the question "Migrate to standalone" sounds like one task, but in a large module-based codebase it is a sequence of decisions: which pieces move first, how long both styles coexist, when the bootstrap changes, and when to stop. There is no single right plan; interviewers want to hear an order that keeps every intermediate state shippable and explains why. ## The order the tooling assumes Angular's own standalone migration is documented as three modes to run in order: 1. **Convert declarations to standalone** — components, directives and pipes stop being `standalone: false` and gain their own `imports`; their modules now import and re-export them. 2. **Remove unnecessary NgModules** — modules that no longer do anything are deleted. 3. **Switch to the standalone bootstrapping API** — `bootstrapModule(AppModule)` becomes `bootstrapApplication(App, appConfig)`. This order is not arbitrary. Each step leaves an app that compiles and runs, because the interop rules allow standalone classes inside modules and modules inside standalone components. The bootstrap switch comes last because it requires the root component itself to be standalone and every root-level provider to have an `ApplicationConfig` home. ## A risk-first plan around that order For a large app owned by several teams, a practical plan wraps the mechanical steps in preparation and verification: 1. **Fix provider hygiene first.** Move singleton services out of shared and feature modules to `providedIn: 'root'`. This removes the duplicate-instance trap that appears when routed standalone components import modules with providers, and it is valuable even if the migration stops here. 2. **Adopt function-based root providers inside `AppModule`.** Replace `HttpClientModule` with `provideHttpClient(...)` and, when convenient, `RouterModule.forRoot` with `provideRouter(...)`. `AppModule.providers` accepts `EnvironmentProviders`. 3. **Write new features standalone.** New code stops adding to the module graph immediately. 4. **Convert existing code leaf-first, one area at a time.** Shared components and pipes with no dependants of their own go first; each converted class is imported (and re-exported) by its old module, so consumers do not change. 5. **Collapse modules per feature.** When a feature's module only re-exports standalone classes, point its route at a `Routes` array and delete the module. 6. **Switch the bootstrap last.** Once `AppModule` is little more than a providers list, move it into `appConfig` and call `bootstrapApplication`. ## Tradeoffs a lead has to own | Decision | Option A | Option B | |---|---|---| | Pace | One big automated pass: fast, one large review | Area by area: slower, reviewable, lower risk | | Coexistence period | Short: less mental overhead | Long: teams move at their own speed | | Library modules you publish | Keep NgModule exports for external consumers | Standalone-only, forcing consumers to adapt | | Stop point | Full removal of `AppModule` | Stop after providers and new code, if the rest is stable and rarely touched | Points worth arguing explicitly: - **Mixed code has a cost.** Two styles mean two sets of conventions in reviews. A long coexistence is fine if it has an end date or a clear rule ("new code standalone, touched code converted"). - **Automation helps but does not decide.** A tool converts code; it does not decide ownership, release windows or which modules are public API. - **Stable, untouched areas may not be worth converting.** Standalone is the recommended default, but NgModules are not deprecated; converting code nobody changes buys little. - **Measure what you claim.** If the goal is simpler code, count modules removed and scope errors avoided; do not promise bundle-size wins without measuring them. ## Verifying each step - The production AOT build must stay green; missing template imports surface as `NG8001`. - Service identity checks (same instance across old and new areas) catch provider regressions. - Each step lands as its own change, so a regression points to one area. ## Communicating the plan A migration that spans teams needs a visible rule set more than a clever order. Useful artefacts are a short written convention ("new code is standalone; any component you touch substantially is converted"), a tracked count of remaining `standalone: false` classes and NgModules per area, and an agreed owner for shared modules, since they are imported by everyone and converted last. That turns the migration from a project into a habit. ## What a strong answer sounds like It names the order (hygiene, new code, leaves, modules, bootstrap), explains why the bootstrap switch is last, acknowledges that stopping early is legitimate, and shows how each step is verified and reversible.
- Why convert leaf components before the modules that use them, rather than top-down?A converted leaf can be imported and re-exported by its old module, so every consumer keeps compiling unchanged. Converting a parent first would require all its children to be importable immediately, which forces a much larger simultaneous change and a bigger review.
- When would you deliberately stop before removing AppModule?When the remaining module-based areas are stable, rarely changed and owned by teams with other priorities. After provider hygiene and standalone-for-new-code, most of the benefit is captured; converting untouched code costs review time and risk for little gain, and NgModules remain supported.
saying these in an interview costs you the question
- Switch to bootstrapApplication first, then convert components
- The migration must be completed in one release to avoid broken states
- NgModules are deprecated, so every module must be removed
- Running the automated migration settles all ownership and ordering decisions
- Standalone migration guarantees smaller bundles without measurement