Angular

Services & Dependency Injection

15 Questions

A service is a class that encapsulates reusable business logic, data access, or utility functions. Services promote separation of concerns by keeping logic out of components and making it reusable. They are typically decorated with @Injectable and injected into components via Angular's dependency injection system. Services are the recommended place for HTTP calls, state management, and shared business logic.
@Injectable({ providedIn: 'root' })
export class UserService {
  private apiUrl = '/api/users';

  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }

  getUserById(id: number): Observable<User> {
    return this.http.get<User>(this.apiUrl + '/' + id);
  }
}
Services are injected into components, directives, pipes, or other services via constructor injection or the inject() function. Using providedIn: 'root' makes the service a singleton across the entire app.

Why it matters: Tests whether you understand Angular's core architecture principle of separation of concerns. Shows you know when and where to move logic out of components to make it reusable and testable across the application.

Real applications: UserService handles all user-related API calls and state. AuthService manages authentication logic independently. DataService fetches and caches data for multiple components. Each service has a single responsibility and is reused throughout the app.

Common mistakes: Developers often put business logic directly in components instead of services, making code hard to reuse. They forget to use providedIn: 'root' thinking services only work in NgModule providers. They also create new service instances repeatedly instead of letting DI manage the singleton.

The @Injectable decorator marks a class as available for Angular's dependency injection system. It optionally specifies where the service should be provided using the providedIn property. Without this decorator, Angular cannot inject the service into other classes. You can provide a service at root level (singleton), module level, or component level (new instance per component).
// Provided at root level (singleton, tree-shakable)
@Injectable({ providedIn: 'root' })
export class AuthService { }

// Provided at module level
@Injectable()
export class LegacyService { }
// Must be added to a module's providers array:
// @NgModule({ providers: [LegacyService] })

// Provided at component level (new instance per component)
@Component({
  providers: [LocalService]
})
export class MyComponent { }
Using providedIn: 'root' is recommended because it creates a singleton and enables tree-shaking. Unused services are automatically removed from the production bundle.

Why it matters: Tests whether you know the recommended way to provide services and understand performance implications like tree-shaking. Shows you grasp modern Angular best practices.

Real applications: Most Angular apps use providedIn: 'root' for global services like AuthService, HttpService, and DataService to ensure they're singletons across the entire app.

Common mistakes: Developers often add services to NgModule providers instead of using providedIn: 'root', missing out on tree-shakability and best practices.

providedIn: 'root' registers the service in the application's root injector, making it a singleton available throughout the entire application. Only one instance of the service is created and shared across all components. It is also tree-shakable, meaning if no component injects the service, it is excluded from the bundle. This is the recommended way to provide services in Angular.
@Injectable({ providedIn: 'root' })
export class CartService {
  private items: Product[] = [];

  addItem(product: Product) { this.items.push(product); }
  getItems() { return this.items; }
  getTotal() { return this.items.reduce((sum, p) => sum + p.price, 0); }
}
Benefits: 1) Singleton shared across all components. 2) Tree-shakable if unused. 3) No need to add it to any module's providers array. Alternative values include 'platform', 'any', or a specific module class.

Why it matters: Tests understanding of Angular's service lifecycle and singleton patterns. Shows you know the most efficient way to provide global services.

Real applications: ShoppingCart service registered with providedIn: 'root' becomes a singleton where every component accesses the same cart data, maintaining consistent state across the app.

Common mistakes: Developers create new service instances repeatedly or mix module-level and root-level providers, causing unexpected behavior when expecting a singleton.

Angular uses a hierarchical dependency injection system with multiple injector levels. Each level can provide its own instance of a service, which overrides parents. The hierarchy goes from root injector down through module injectors to component injectors. When a component requests a dependency, Angular walks up the tree until it finds a matching provider.
// Root-level: singleton for entire app
@Injectable({ providedIn: 'root' })
export class GlobalService {}

// Module-level: shared within the module
@NgModule({
  providers: [ModuleScopedService]
})
export class FeatureModule {}

// Component-level: new instance for each component
@Component({
  selector: 'app-panel',
  providers: [PanelService]
})
export class PanelComponent {
  constructor(private panelService: PanelService) {}
}
The lookup order is: componentparent componentmoduleroot. The first matching provider wins. Component-level providers create new instances per component.

Why it matters: Tests your deeper understanding of Angular's dependency resolution mechanism. Shows you can explain how Angular searches for providers and handles provider precedence.

Real applications: A feature module might override a global service with its own version. A component needs a local state service separate from the app-wide service, so it provides a component-level version.

Common mistakes: Developers don't realize component-level providers create new instances, thinking all components still share the same service. They also don't understand override behavior through the hierarchy.

An InjectionToken is used to provide non-class dependencies like configuration values, strings, or interfaces via dependency injection. Since TypeScript interfaces are erased at runtime, you cannot use them directly as DI tokens. InjectionToken creates a unique token object that Angular can use to look up the value. You provide values using the useValue provider and inject them with @Inject.
// Define token
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');

// Provide value
@NgModule({
  providers: [
    { provide: API_BASE_URL, useValue: 'https://api.example.com' },
    { provide: APP_CONFIG, useValue: { debug: false, version: '1.0' } }
  ]
})
export class AppModule {}

// Inject with @Inject
constructor(@Inject(API_BASE_URL) private apiUrl: string) {}
InjectionTokens prevent naming collisions when multiple providers use simple types. They are essential for injecting interfaces, primitives, and configuration objects.

Why it matters: Tests whether you understand how to work with non-class dependencies and TypeScript interfaces in the DI system. Shows you know Angular patterns for configuration and complex types.

Real applications: API_BASE_URL token provides the backend URL. APP_CONFIG token provides global app settings. FEATURE_FLAGS token provides feature toggles, all injected where needed without type conflicts.

Common mistakes: Developers try to inject interfaces directly instead of using InjectionToken. They forget to use @Inject decorator when injecting tokens, confusing them with class-based services.

These are provider configuration options that control how Angular resolves a dependency when it is injected. useClass provides a class instance, useValue provides a static value, useFactory uses a function, and useExisting creates an alias. They allow you to customize how the DI system creates and delivers dependencies. This flexibility is essential for swapping implementations and providing configuration.
// useClass: provide a class (can swap implementations)
{ provide: LoggerService, useClass: DebugLoggerService }

// useValue: provide a static value
{ provide: API_URL, useValue: 'https://api.example.com' }

// useFactory: provide via a factory function
{
  provide: DataService,
  useFactory: (http: HttpClient, config: AppConfig) => {
    return config.useMock ? new MockDataService() : new RealDataService(http);
  },
  deps: [HttpClient, APP_CONFIG]
}

// useExisting: alias one token to another
{ provide: AbstractLogger, useExisting: ConsoleLoggerService }
useClass creates a new instance. useValue provides a constant value. useFactory runs a function with optional deps. useExisting aliases one token to another existing provider.

Why it matters: Tests understanding of advanced provider configuration patterns. Shows you can explain different ways to customize dependency resolution and when to use each.

Real applications: useFactory is used to return different DataService (Mock vs Real) based on environment. useExisting aliases AbstractLogger to ConsoleLoggerService. useValue provides configuration objects.

Common mistakes: Developers confuse useClass with useValue, not realizing useClass instantiates the class while useValue uses it as-is. They forget to specify deps array in useFactory when the function has dependencies.

Multi providers allow multiple values to be registered under the same injection token. Angular collects all values into an array when the token is injected. This is useful when you need to extend a feature from multiple places, like adding validators or interceptors. Without the multi: true flag, each new provider would replace the previous one.
const VALIDATORS = new InjectionToken<Validator[]>('VALIDATORS');

@NgModule({
  providers: [
    { provide: VALIDATORS, useClass: RequiredValidator, multi: true },
    { provide: VALIDATORS, useClass: EmailValidator, multi: true },
    { provide: VALIDATORS, useClass: MinLengthValidator, multi: true }
  ]
})
export class AppModule {}

// Injection returns an array
@Injectable()
export class FormService {
  constructor(@Inject(VALIDATORS) private validators: Validator[]) {
    // validators = [RequiredValidator, EmailValidator, MinLengthValidator]
  }
}
The multi: true flag tells Angular to add the provider to a collection rather than replacing previous ones. Angular uses this pattern internally for HTTP_INTERCEPTORS and APP_INITIALIZER.

Why it matters: Tests whether you understand extensible provider patterns and real Angular framework patterns used for interceptors and initialization hooks.

Real applications: Multiple HTTP_INTERCEPTORS are registered with multi: true so each one processes requests. Multiple validators are collected into an array. Multiple APP_INITIALIZERs run before app startup.

Common mistakes: Developers forget to use multi: true and accidentally override previous validators. They don't realize multi providers collect into arrays rather than singular values.

The @Optional decorator tells Angular to return null instead of throwing an error if a dependency is not found in the injector tree. Without it, Angular throws a NullInjectorError when it cannot resolve the dependency. This is useful for plugins, optional features, or services that may not always be registered. In Angular 14+, you can also use the inject() function with an optional flag.
@Injectable()
export class NotificationService {
  constructor(
    @Optional() private analytics: AnalyticsService
  ) {}

  notify(msg: string) {
    console.log(msg);
    // Only track if analytics is available
    if (this.analytics) {
      this.analytics.track('notification', msg);
    }
  }
}

// Or using inject() function (Angular 14+)
export class MyComponent {
  private analytics = inject(AnalyticsService, { optional: true });
}
@Optional is essential for building flexible components that work with or without certain services. Always check for null before using an optional dependency to avoid runtime errors.

Why it matters: Tests knowledge of advanced DI patterns and how to build robust applications that gracefully handle missing dependencies. Shows architectural thinking about optional features.

Real applications: AnalyticsService might be optional in development but required in production. A notification service works without an analytics service, just tracking silently if available. Plugin systems use optional dependencies for extensions.

Common mistakes: Developers forget to check if the optional dependency is null before using it, causing runtime errors. They think @Optional magically handles null checking, but it only prevents errors on injection.

These decorators control how far Angular searches the injector hierarchy when resolving a dependency. @Self() restricts the lookup to only the current component's injector. @SkipSelf() skips the current injector and starts from the parent. @Host() limits the search up to the host component's boundary and no further.
@Component({
  selector: 'app-child',
  providers: [LocalService]
})
export class ChildComponent {
  constructor(
    // Only look at this component's injector
    @Self() private local: LocalService,

    // Skip this component, look at parent injectors only
    @SkipSelf() private parentService: ParentService,

    // Stop at the host component's injector (no further)
    @Host() private hostService: HostService
  ) {}
}
@Self() restricts to the current injector. @SkipSelf() starts from the parent. @Host() stops at the host boundary. Combine with @Optional to avoid errors when a provider is not found at the restricted scope.

Why it matters: Tests deep understanding of Angular's injector hierarchy. Shows you can control how DI resolution works and handle advanced cases.

Real applications: A parent component provides a service, but a child component wants its own version using @Self. A dialog component using @Host restricts injection to its host. @SkipSelf skips local overrides to use parent or root versions.

Common mistakes: Developers don't understand the difference between these decorators. They use @Self when they should use @SkipSelf or vice versa. They forget @Optional goes with restriction decorators to handle missing providers gracefully.

Tree-shakable providers are services that can be removed from the final bundle if no component or service injects them. They are created by using providedIn in the @Injectable decorator instead of listing in module providers. The bundler can detect unused services and exclude them, reducing the bundle size. This is one of the main advantages of using providedIn over module-level providers.
// Tree-shakable: removed from bundle if unused
@Injectable({ providedIn: 'root' })
export class UnusedService {
  doWork() { return 'work'; }
}

// NOT tree-shakable: always included because listed in module providers
@Injectable()
export class AlwaysIncludedService {
  doWork() { return 'work'; }
}
@NgModule({
  providers: [AlwaysIncludedService]  // always in bundle
})
export class AppModule {}
With providedIn: 'root', the service references the injector rather than the injector referencing the service. This inverted dependency allows bundlers to tree-shake services that nothing imports.

Why it matters: Tests knowledge of optimal Angular patterns and build optimization. Shows you understand how modern bundlers reduce bundle size.

Real applications: Logging service never used in production is removed via tree-shaking. Analytics service not imported by any component gets eliminated. Experimental feature services get stripped from bundles if not used.

Common mistakes: Developers provide all services in modules, missing out on tree-shaking benefits and larger bundles. They don't realize tree-shaking only works with providedIn declarations, not module providers.

When you use providedIn: 'root', the service is a singleton available throughout the entire application and is tree-shakable. When you provide a service in a module's providers array, it is always included in the bundle regardless of usage. Lazy-loaded modules with providers create their own injector, so a service provided there gets a separate instance. Root-level provision is the recommended approach for most services.
// Recommended: tree-shakable, singleton
@Injectable({ providedIn: 'root' })
export class GlobalService { }

// Module-level: always in bundle, not tree-shakable
@NgModule({
  providers: [ModuleService]
})
export class FeatureModule { }
providedIn: 'root' is preferred for most services because of tree-shaking and singleton behavior. Use module-level providers only when you need a separate instance per lazy-loaded module.

Why it matters: Tests understanding of architectural patterns and performance implications. Shows knowledge of when to use each pattern for optimal results.

Real applications: Global AuthService uses providedIn: 'root'. Feature modules provide their own CartService for isolated checkout flows. Settings service uses root to share app-wide configuration.

Common mistakes: Developers mix both patterns in the same app. They provide in modules when they should use providedIn: 'root', losing tree-shaking. They provide in lazy modules with providedIn: 'root', creating unexpected multiple instances.

The inject() function is an alternative to constructor-based dependency injection introduced in Angular 14. It can be used in constructors, field initializers, and factory functions. It allows injecting dependencies outside of the constructor, making code more concise. It is especially useful in functional guards, interceptors, and resolvers where there is no class constructor.
// Field-level injection
export class UserComponent {
  private http = inject(HttpClient);
  private router = inject(Router);
  private config = inject(APP_CONFIG, { optional: true });
}

// In functional guard
export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  return auth.isLoggedIn();
};
The inject() function must be called in an injection context (constructor, field initializer, or factory). It supports options like optional, self, and skipSelf.

Why it matters: Tests knowledge of modern Angular DI patterns introduced in Angular 14. Shows you can use functional programming and the latest framework features.

Real applications: Functional guards use inject() to access services. Factory functions use inject() to configure providers. Standalone components use inject() for cleaner dependency injection.

Common mistakes: Developers try to use inject() outside injection contexts, causing errors. They confuse inject() with basic DI, forgetting it must be in specific contexts. They don't realize inject() is the preferred pattern for functional features.

When a service is provided at both root and component level, the component gets its own separate instance. Angular's injector hierarchy first looks at the component's own providers, then walks up to parent components, modules, and root. A component-level provider overrides the root-level one for that component and all its children. This is useful when you need isolated state for a specific component subtree.
// Root level: shared singleton
@Injectable({ providedIn: 'root' })
export class CounterService { count = 0; }

// Component level: each instance gets its own CounterService
@Component({
  selector: 'app-counter',
  providers: [CounterService], // separate instance
  template: '<p>{{ counter.count }}</p>'
})
export class CounterComponent {
  constructor(public counter: CounterService) {}
}
The component-level instance is isolated from the root singleton. Other components still use the root instance unless they also provide the service at their own level.

Why it matters: Tests understanding of how Angular's hierarchical injector system works. Shows you can predict and control instance creation for different scopes.

Real applications: Dialog service provided at dialog component level has isolated state. Each modal gets its own store instance. Nested components override parent services with local versions for independence.

Common mistakes: Developers unknowingly provide the same service at multiple levels, creating unexpected instances. They assume component-level providers still share root state, causing hard-to-debug inconsistencies.

useClass creates a brand new instance of the specified class when the token is requested. useExisting creates an alias to an already existing provider — both tokens point to the same instance. Use useClass when you want to swap one service implementation for another, like replacing a real service with a mock. Use useExisting when you want two different tokens to resolve to the same service instance.
// useClass: creates NEW instance of MockApiService
{ provide: ApiService, useClass: MockApiService }

// useExisting: both tokens point to SAME instance
{ provide: AbstractLogger, useExisting: ConsoleLoggerService }

// Example: one instance shared via two tokens
providers: [
  ConsoleLoggerService,
  { provide: AbstractLogger, useExisting: ConsoleLoggerService }
]
useClass is for swapping implementations (e.g., mock vs real). useExisting is for creating token aliases that share the same underlying service instance.

Why it matters: Tests knowledge of subtle but important provider patterns. Shows understanding of when to create new instances vs when to reuse existing ones.

Real applications: useClass replaces ApiService with MockApiService in tests. useExisting makes AbstractLogger an alias for ConsoleLoggerService. Both patterns are used for flexibility and interface-based programming.

Common mistakes: Developers confuse useClass with useExisting, not realizing useClass creates new instances while useExisting shares. They use useClass when they need token aliasing or vice versa.

A singleton service has only one instance shared across the entire application. When you use providedIn: 'root' or add the service to the AppModule providers, Angular creates one instance and gives the same reference to every component. This is useful for shared state like authentication status, shopping cart data, or application settings. Be careful not to provide the same service in a lazy-loaded module as it creates a second instance.
@Injectable({ providedIn: 'root' }) // singleton
export class CartService {
  private items: Product[] = [];
  addItem(item: Product) { this.items.push(item); }
  getItems() { return this.items; }
}

// Both components share the same CartService instance
// Changes made by one component are visible to the other
The providedIn: 'root' pattern guarantees a singleton across the app. Avoid providing the same service in lazy-loaded modules as it breaks the singleton pattern by creating separate instances.

Why it matters: Tests fundamental understanding of state management architecture in Angular. Shows you know how to prevent common bugs with shared state.

Real applications: AuthService as a singleton manages login state across the entire app. UserPreferencesService stays consistent everywhere. ShoppingCartService is a singleton so all pages see the same cart.

Common mistakes: Developers provide the same service in multiple lazy-loaded modules, accidentally creating separate instances. They rely on singletons without realizing multiple instances were created. They don't use providedIn: 'root' and miss the singleton guarantee.