Services and DI: Shared Capabilities, Scoped Correctly In short: Anything that is not about pixels belongs in a service. Angular's job is to hand you that service at the right scope — and in Angular 22 there is a shorter way to ask. Components manage what appears on screen. Directives customise how elements behave. So where does the code that does not care about HTML go? Where do you calculate government milk pricing subsidies, fetch chilling-centre sensor readings, or track which supervisor is logged in? Putting that in a component makes it bloated and hard to test. In Angular, non-visual capability lives in services , and dependency injection delivers them where they are needed. What dependency injection actually buys you Without DI, a component that needs payout maths builds its own tools: With DI, the component states what it needs and Angular supplies it: In a test you can provide a fake without touching the component. That is the whole point: the component depends on a shape , not on a specific object. Note rather than constructor parameters. Angular's current guidance is to use the function; it works in field initialisers, composes into reusable functions, and does not force you to write a constructor you did not otherwise need. Writing a service in Angular 22 Angular 22 introduced the decorator as a shorthand for the pattern almost everyone was already writing. The guidance is to prefer it for new singletons: and do the same thing. just says what you meant. is still correct and still required when you need other options — a factory, a non-root scope, or . Choosing a lifetime on purpose The most consequential decision about a service is not what it does. It is how long it lives . Root scope — one instance for the whole app (or ) gives you a single instance shared by everything. Good for: authentication, HTTP clients, app-wide settings, reference data. Dairy example: tracking the logged-in supervisor, or above. Root-provided services are also tree-shakable. If nothing injects it, it never reaches the bundle. Component scope — a fresh instance per component Put a service in a component's array and Angular creates a brand-new instance for that component and its children. When the component is destroyed, so is the service. Why bother? Picture an operator with two intake wizards open side by side, recording from two different tankers. If were a root singleton, the second wizard would overwrite the first one's data. Scoping it to the component gives each wizard its own sandbox, and cleans up automatically on close. Route scope — one instance per lazy-loaded feature You can also provide a service on a route, which ties its lifetime to that feature: The state exists while the user is inside tanker intake and is discarded when they navigate away. Loading a service lazily with Some capabilities are heavy and rarely used. A PDF payout-statement generator might pull in a large formatting library that most users never touch. Angular 22's splits that service into its own bundle chunk and downloads it the first time you ask: You can start the download earlier with a prefetch trigger, for example , so the chunk is usually already there by the time someone clicks. One requirement: the target service must be auto-provided — or . A service listed in a component's cannot be loaded this way. "But I thought a service with state should always be a root singleton" This is the most common DI mistake, and it comes from a reasonable place: "single source of truth" is good advice, and a singleton is the most literal way to get one. The problem is that "single source of truth" applies to a piece of data , not to a category of service. Two different tankers being weighed at the same time are not one truth being contested — they are two truths that happen to have the same shape. The bug this produces looks like data corruption and gets blamed on the backend: Operator A opens a wizard for TNK-402 and enters three farmers. Operator A then opens a second wizard for TNK-118 in another tab of the same app. is now — and the three entries still sitting in get submitted against the wrong tanker. Nothing threw. The data is simply wrong, and the milk is now recorded against the wrong route. Scoping it to the component makes the bug structurally impossible, because there are two instances holding two independent drafts. The useful question is not "is this shared?" It is: when this thing goes away, should its data go away too? If yes, scope it to whatever goes away. Data Scope Why --- --- --- Logged-in supervisor Root ( ) One per browser session Milk price table from the dairy board Root Same for everyone Draft entries in an intake wizard Component Dies with the wizard Filters on the dispatch dashboard Route Dies when you leave the feature Which accordion panel is open Not a service — local Nobody else needs it Separation of concerns Components — presentation, user events, rendering state. Directives — element behaviour, host bindings, intera…