Services & Dependency Injection in Angular

medium · Angular

The Core Logic and Provisioning Layer In a modern component-driven web architecture, components should strictly handle view presentation and user interaction logic. Mixing data-fetching calculations, state synchronization loops, or infrastructure tasks directly into a component's code creates monolithic, unmaintainable systems. To enforce a strict separation of concerns, frameworks decouple this architecture into two complementary layers: Services and Dependency Injection (DI) . Services: Encapsulating Business Logic A Service is a broad category encompassing any class, value, or function that sits outside the UI presentation layer. Services house the core operational business logic of an heavy web application. Modularity and Sharing: Services isolate repetitive task code—such as calling a REST API endpoint, logging diagnostic details, or reading environment variables. Instead of duplicating this code across multiple pages, you isolate it in a clean service layer that any component can consume. State Preservation: Unlike components, which are continuously instantiated and destroyed as users move across routes, services can remain active throughout the entire lifecycle of an application, serving as a reliable memory plane to cache data and preserve global states. TypeScript // A pure business logic data fetcher export class JobDataService { fetchHandpickedJobs() { return ['Software Developer', 'UI Designer', 'DevOps Specialist']; } } Dependency Injection (DI): The Wiring Engine Dependency Injection is a design pattern used to achieve Inversion of Control (IoC) between classes and their dependencies. Instead of a component manually instantiating its dependent services inside its constructor using the explicit new keyword, the component simply declares its required dependencies. The framework’s central DI engine handles instantiating and injecting those dependencies at runtime. Why Avoid the new Keyword? If a component instantiates a service manually using const service = new JobDataService() , it becomes tightly coupled to that specific class. If the service constructor is refactored later to require a configuration parameter (e.g., constructor(private apiToken: string) ), every single component using that class breaks instantly. Tightly coupled classes make testing difficult, as you cannot mock or substitute the service with a dummy testing double during isolated component unit tests. The Framework Class Injection Blueprint TypeScript import { Component, Injectable, inject } from '@angular/core'; // The decorator turns the plain class into an injectable dependency target @Injectable({ providedIn: 'root' }) export class ModernLoggerService { logEvent(message: string): void { console.log(`[SYSTEM LOG]: ${message}`); } } @Component({ selector: 'app-dashboard-view', standalone: true, template: `<button (click)="runAction()">Trigger</button>` }) export class DashboardViewComponent { // Method 1: Modern Functional Injection API (Recommended) private logger = inject(ModernLoggerService); // Method 2: Traditional Constructor-Based Injection Parameters // constructor(private logger: ModernLoggerService) {} runAction(): void { this.logger.logEvent('Dashboard interaction recorded.'); } } In stantiating the Ecosystem: Singleton Services When you configure a service with the metadata flag @Injectable({ providedIn: 'root' }) , you are registering it with the global Root Injector . This designates the class as a Singleton Service .\ Single Memory Allocation: Angular creates exactly one unique instance of this service class for the entire application lifecycle. Shared State Memory: Every single component or service that injects this root singleton interacts with the exact same memory instance. If Component A writes a data value into a tracker property inside a singleton service, Component B can read that modified state immediately, establishing a lightweight, localized state management channel. # Singleton Execution Plane [Global Root Injector Engine] ── Instantiates Once ──► [JobDataService Instance] │ ┌─────────────────────────┬───────────────────────────────┴──────────────────────────────┐ ▼ ▼ ▼ [Component A] [Component B] [Component C] (Shares Instance) (Shares Instance) (Shares Instance) Abstracting Values and Primitives: Injection Tokens Dependency Injection works out of the box when you inject standard class types because TypeScript preserves class names at runtime, allowing the DI engine to use the class type as an tracking lookup key. However, interfaces, configuration objects, environment strings, or raw primitive variables do not map to runtime class instances. When compiled down to pure JavaScript, TypeScript interface contracts disappear entirely. To inject these abstract primitives or configurations securely, you use an InjectionToken . [Image workflow of Injection Tokens mapping abstract configuration objects into the framework DI lookup registry] Complete Token Implementation Configuration The following implementation details how to compile, register, and safely inject a custom, read-only configuration payload model using a custom framework InjectionToken . TypeScript // 1. Define the structural contract type and instantiate the explicit Injection Token import { InjectionToken, Injectable, inject, Component } from '@angular/core'; export interface AppConfigPayload { apiEndpointUrl: string; productionMode: boolean; } // Creating the tracking key token for the DI registry system export const APP_CONFIG = new InjectionToken<AppConfigPayload>('app.environment.config'); // 2. Register the literal value provider inside the global configuration manifest (app.config.ts) export const appConfigProviderValue: AppConfigPayload = { apiEndpointUrl: 'https://api.careeroza.com/v1', productionMode: false }; // Inside app.config.ts provider array: // providers: [ { provide: APP_CONFIG, useValue: appConfigProviderValue } ] // 3. Injecting the non-class token dependency inside an application service layer @Injectable({ providedIn: 'root' }) export class NetworkCommunicationEngine { // Utilizing the token key explicitly inside the functional inject method private environmentSettings = inject(APP_CONFIG); makeSecureRequest(): string { return `Connecting out to server target: ${this.environmentSettings.apiEndpointUrl}/jobs`; } } Dependency Provisioning Resolution Matrix Resolution Property @Injectable({ providedIn: 'root' }) { provide: TOKEN, useValue: ... } Primary DI Key Type Standard class reference types. Specialized framework InjectionToken instances. Target Data Shape Executable application services or state stores containing business methods. Non-class configurations, string credentials, or primitive data objects. Instantiated Mode Automated on-demand initialization handled completely by the compiler. Hand-mapped values bound manually within configuration metadata arrays. Tree-Shaking Profile Excellent. If zero components actively inject the service class, the compiler strips it out of the build. Depends on placement; if imported globally inside core manifests, it remains compiled.

Back to Angular

Browse all study material on Careeroza