DEV Community

Cover image for @Service: Angular 22's @Service Decorator: Say Goodbye to @Injectable Boilerplate - Here’s What Changes (Part 4)
Rajat
Rajat

Posted on

@Service: Angular 22's @Service Decorator: Say Goodbye to @Injectable Boilerplate - Here’s What Changes (Part 4)

A practical look at Angular's new, opinionated way to write services — what changed, why it matters, and when you should still reach for the old decorator

Quick question before we start: how many times have you typed @Injectable({ providedIn: 'root' }) this month without thinking twice about it?

If you've been writing Angular for more than a few weeks, the answer is probably "too many to count." It's one of those patterns we type on autopilot — copy, paste, rename the class, move on. Angular 22 finally does something about that muscle memory, and it's called @Service.

In this article we're going to unpack the new decorator like two developers pairing on a Friday afternoon: no fluff, just the "why," the "how," and the "when should I actually use this." By the end, you'll know:

  • Why the Angular team added @Service when @Injectable already worked fine
  • Exactly how @Service differs from @Injectable, with a side-by-side comparison
  • How to use the factory and autoProvided options for more advanced cases
  • How to write unit tests for services built with @Service
  • When you should skip @Service and stick with the classic decorator

Sound good? Follow along, and if you find this useful, consider following me here on Medium — I write about practical Angular and React patterns every week, not just "what's new" roundups.

Before we dive into the examples, a quick note: The code snippets provided here are meant purely for understanding the concept. Some syntax shown may reflect patterns from earlier Angular/React versions. Always refer to the official documentation for the most current API and syntax.

Why Angular introduced @Service

Let's start with the pattern we all know. Here's a completely typical Angular service, written the way we've written it for years:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  getUsers() {
    return this.http.get('/api/users');
  }
}
Enter fullscreen mode Exit fullscreen mode

Nothing wrong with this. It works, it's familiar, and it's been the backbone of Angular DI since the framework's early days. But look closely at what's actually happening: @Injectable was built to describe how Angular should construct and provide a class, with support for useClass, useValue, useExisting, useFactory, non-root scopes, and constructor injection. Almost none of that flexibility gets used in a typical app service. Most of the time you just want a plain singleton that lives at the root of your app.

That mismatch — a powerful, general-purpose decorator being used for the simplest possible case — is exactly what @Service is designed to fix. It's a more opinionated decorator built around the assumption that you're creating a root-provided singleton and injecting your dependencies with the inject() function rather than a constructor.

Here's the same service, rewritten with @Service:

import { Service, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Service()
export class UserService {
  private http = inject(HttpClient);

  getUsers() {
    return this.http.get('/api/users');
  }
}
Enter fullscreen mode Exit fullscreen mode

Same behavior, same singleton guarantees — Angular still gives you a single shared instance, automatic availability across the app, and tree-shakability if the service is never actually used. What's gone is the ceremony: no providedIn: 'root' to remember, no constructor parameter list to keep in sync every time a dependency changes.

Ever caught yourself forgetting providedIn: 'root' and wondering why you got a NullInjectorError at runtime? That's precisely the kind of small, avoidable mistake @Service removes from the equation, since root provisioning is now the default rather than something you opt into.

@Service vs @Injectable: what's actually different

It helps to see the two side by side instead of just reading about them. Here's how they compare:

Feature @Service @Injectable
inject() function support Yes Yes
Constructor-based DI No Yes
Root singleton by default Yes No — requires providedIn: 'root'
Advanced provider keys (useClass, useValue, etc.) No Yes
Custom initialization via factory Yes, via factory Yes, via useFactory
Non-root scopes (platform, etc.) No Yes

The biggest mental shift is that @Service only supports inject() for dependencies — there's no constructor injection at all. If your team has already standardized on inject() (which most modern Angular codebases have, especially since standalone components became the default), this isn't a big adjustment. If you're still writing constructor-based DI throughout your app, this is a good nudge to modernize.

Using a factory for conditional logic

Sometimes a service's implementation needs to change based on environment or configuration — think analytics that should be a no-op locally but forward events to a real tracker in production. @Service handles this with a single factory option that runs inside an injection context, so you can call inject() right inside it:

import { inject, InjectionToken, Service } from '@angular/core';

export const ANALYTICS_ENABLED = new InjectionToken<boolean>('ANALYTICS_ENABLED');

@Service({
  factory: () =>
    inject(ANALYTICS_ENABLED) ? new GoogleAnalytics() : new Analytics(),
})
export class Analytics {
  track(event: string, payload?: Record<string, unknown>) {
    // No-op by default.
  }
}

class GoogleAnalytics extends Analytics {
  override track(event: string, payload?: Record<string, unknown>) {
    // Sends the event to Google Analytics in production.
  }
}
Enter fullscreen mode Exit fullscreen mode

The factory option is meant to replace useClass, useValue, useExisting, and useFactory from @Injectable. If your use case needs one of those specifically, that's your signal to reach for @Injectable instead — @Service is intentionally not trying to cover every advanced scenario.

Opting out of automatic root provisioning

Not every service should live at the root. If you want a service scoped to a specific route or component instead, set autoProvided: false and register it manually, the same way you would with @Injectable:

import { Service } from '@angular/core';

@Service({ autoProvided: false })
export class AnalyticsLogger {
  trackEvent(name: string) {
    console.log('event:', name);
  }
}
Enter fullscreen mode Exit fullscreen mode

You'd then add AnalyticsLogger to the providers array of whichever component or route should own its lifecycle — nothing new here if you've scoped services before, just a slightly different starting point.

Using the service in a component

Injecting a @Service-decorated class works exactly the way you'd expect if you're already using inject():

import { Component, inject } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  template: `
    @if (users(); as list) {
      @for (user of list; track user.id) {
        <p>{{ user.name }}</p>
      }
    } @else {
      <p>Loading users...</p>
    }
  `,
})
export class UserList {
  private userService = inject(UserService);
  users = this.userService.getUsers();
}
Enter fullscreen mode Exit fullscreen mode

Notice the @if / @for control flow syntax here instead of *ngIf / *ngFor — that's the modern Angular template syntax, and it pairs naturally with the rest of this more streamlined, signal-friendly style of writing components.

Writing unit tests for a @Service class

Good news: testing a @Service class is nearly identical to testing an @Injectable one, since TestBed doesn't care which decorator produced the provider — it just resolves the dependency graph. Here's a straightforward test for the UserService from earlier:

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
    });

    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should fetch users from the API', () => {
    const mockUsers = [{ id: 1, name: 'Ada Lovelace' }];

    service.getUsers().subscribe((users) => {
      expect(users).toEqual(mockUsers);
    });

    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });
});
Enter fullscreen mode Exit fullscreen mode

If you're testing a service created with autoProvided: false, just remember it won't be picked up automatically — you'll need to add it to the providers array of your TestBed.configureTestingModule call, the same way you would in a real component:

TestBed.configureTestingModule({
  providers: [AnalyticsLogger],
});
Enter fullscreen mode Exit fullscreen mode

That's really the whole story for testing: the decorator changes how the service is provided, not how it behaves once it's resolved, so your existing testing habits carry over almost untouched.

What's your usual setup for mocking HTTP calls in service tests — HttpClientTestingModule, a hand-rolled spy, or something else? I'd genuinely like to know what's working for people right now.

Best practices for adopting @Service

A few things worth keeping in mind as you start using this in real projects:

  • Migrate incrementally. You don't need to convert every service in your app at once. @Service and @Injectable interoperate fine, so it's safe to introduce the new decorator only in new files and gradually touch old ones as you edit them anyway.
  • Pair it with inject() everywhere. Since @Service doesn't support constructor injection, standardizing on inject() across your whole codebase — components included — keeps things consistent and avoids a codebase with two competing DI styles.
  • Use factory sparingly. It's there for genuine conditional construction, not as a place to smuggle in business logic. If your factory function is getting long, that's usually a sign the logic belongs inside the class itself.
  • Reach for autoProvided: false deliberately. Scoping a service to a component is a real architectural decision — make sure it's intentional rather than a copy-pasted default.

When not to use @Service

@Service is not a full replacement for @Injectable, and that's fine — it was never meant to be. Stick with @Injectable when you need:

  • Constructor-based dependency injection, for example in a codebase that hasn't migrated to inject() yet
  • Advanced provider configuration such as useClass, useValue, or useExisting
  • A non-root scope like providedIn: 'platform'

If none of those apply, @Service is generally the more direct, more modern choice for a new singleton class.

Bonus tips

  • If you're using an editor with Angular language service support, decorator autocompletion for @Service should already suggest factory and autoProvided as you type — a quick way to double-check you're not misremembering the option names.
  • Consider adding an ESLint rule to your project that flags new @Injectable({ providedIn: 'root' }) usages without useClass/useFactory/etc., nudging your team toward @Service for the simple cases automatically.
  • When reviewing pull requests, a quick heuristic is: if the diff adds a constructor purely to inject one or two dependencies, that's often a good candidate to rewrite with inject() and @Service in the same pass.

Recap

@Service is Angular 22's answer to years of typing the same @Injectable({ providedIn: 'root' }) boilerplate for services that never needed the extra configuration in the first place. It defaults to a root-provided singleton, drops constructor injection in favor of inject(), and replaces the old provider keys with a single factory option for the cases that genuinely need one. It's not trying to replace @Injectable entirely — for constructor DI, advanced provider configuration, or non-root scopes, @Injectable is still the right tool. But for the everyday singleton service, which is most of them, @Service is a small change that removes a surprising amount of repetitive code.

Did this approach match how you've been writing services, or are you planning to hold off on adopting it for a while? Either way, I'd like to hear where you land.


What did you think?
Did this approach match how you are solving it, or do you have a different take? Drop a comment — I genuinely read every single one.

Found this helpful?
If this saved you even a few minutes of debugging or confusion, hit that clap button so others can find it too. It really does make a difference.

Want more tips like this?
I share one practical dev insight every week. Follow me here on Medium or subscribe to my newsletter so you never miss one.

Let us connect — find me on LinkedIn or GitHub and let us keep the conversation going.


Follow Me for More Angular & Frontend Goodness:

I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.

  • 💼 LinkedIn — Let’s connect professionally
  • 🎥 Threads — Short-form frontend insights
  • 🐦 X (Twitter) — Developer banter + code snippets
  • 👥 BlueSky — Stay up to date on frontend trends
  • 🌟 GitHub Projects — Explore code in action
  • 🌐 Website — Everything in one place
  • 📚 Medium Blog — Long-form content and deep-dives
  • 💬 Dev Blog — Free Long-form content and deep-dives
  • ✉️ Substack — Weekly frontend stories & curated resources
  • 🧩 Portfolio — Projects, talks, and recognitions
  • ✍️ Hashnode — Developer blog posts & tech discussions
  • ✍️ Reddit — Developer blog posts & tech discussions

Top comments (0)