DEV Community

Cover image for Angular Services and Dependency Injection: Complete Guide | Singleton Services & Service Patterns
Md. Maruf Rahman
Md. Maruf Rahman

Posted on • Originally published at marufrahman.live

Angular Services and Dependency Injection: Complete Guide | Singleton Services & Service Patterns

When I first started building Angular applications, I put all my business logic directly in components. That worked fine for small apps, but as applications grew, I found myself duplicating code, struggling with component communication, and making components harder to test. That's when I discovered Angular Services and Dependency Injection, and it completely changed how I structure applications.

Angular Services are singleton classes that provide reusable business logic, data access, and shared functionality across your application. They use Angular's Dependency Injection system, which means you don't manually create service instancesβ€”Angular does it for you and provides them wherever you need them. This makes your code more modular, testable, and maintainable.

πŸ“– Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.

What are Angular Services?

Angular Services provide:

  • Reusable business logic - Share code across components
  • Data access layer - Centralize API calls and data operations
  • State management - Share state between components
  • Singleton pattern - Single instance shared across the app
  • Dependency Injection - Automatic service provisioning
  • Testability - Easy to mock and test independently
  • Separation of concerns - Keep components focused on presentation

Creating a Basic Service

Create a service using the @Injectable decorator:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpHelper } from 'src/core/factory/http.helper';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class BusinessService {
  private url: string;

  constructor(private http: HttpHelper) {
    this.url = `${environment.ApiUrl}business`;
  }

  public GetBusinesses(data: any): Observable<any> {
    return this.http.post(`${this.url}/get`, data);
  }

  public GetBusiness(businessId: number): Observable<any> {
    return this.http.get(`${this.url}/${businessId}/details`);
  }

  public SaveBusiness(data: any, businessId: number): Observable<any> {
    return this.http.post(`${this.url}/${businessId}/details`, data);
  }

  public DeleteBusiness(id: number): Observable<any> {
    return this.http.delete(`${this.url}/${id}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • @Injectable decorator marks the class as a service
  • providedIn: 'root' makes it an application-wide singleton
  • Services can inject other services through constructor injection
  • Services are instantiated once and shared across the app

Using Services in Components

Inject services into components via constructor:

import { Component, OnInit } from '@angular/core';
import { BusinessService } from 'src/services/business.service';
import { ToastService } from 'src/core/controls/toast-global/toast.service';

@Component({
  selector: 'app-business-list',
  templateUrl: './business-list.component.html',
  styleUrls: ['./business-list.component.scss']
})
export class BusinessListComponent implements OnInit {
  public businesses: any[] = [];
  public loading: boolean = false;

  constructor(
    private businessService: BusinessService,
    private toast: ToastService
  ) {}

  ngOnInit(): void {
    this.loadBusinesses();
  }

  private loadBusinesses(): void {
    this.loading = true;
    this.businessService.GetBusinesses({ pageNumber: 1, pageSize: 10 })
      .subscribe({
        next: (response) => {
          this.businesses = response.data;
          this.loading = false;
        },
        error: (error) => {
          this.toast.error(error);
          this.loading = false;
        }
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

Dependency Injection Process:

  1. Declare service in constructor with private or public accessor
  2. Angular's injector automatically provides the service instance
  3. Use the service throughout the component
  4. No need to manually instantiate services

Service with HTTP Client

Services commonly handle HTTP requests:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap, catchError } from 'rxjs/operators';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private url: string;

  constructor(private http: HttpClient) {
    this.url = `${environment.ApiUrl}users`;
  }

  public SearchUsers(searchTerm: string, isAll: boolean = false): Observable<any[]> {
    if (!searchTerm || searchTerm.length < 3) {
      return of([]);
    }

    return this.http.post(`${this.url}/search?isAll=${isAll}`, { search: searchTerm })
      .pipe(
        map((response: any) => response.data || []),
        tap(users => console.log('Found users:', users)),
        catchError(error => {
          console.error('Search error:', error);
          return of([]);
        })
      );
  }

  public GetUser(userId: number): Observable<any> {
    return this.http.get(`${this.url}/${userId}`);
  }

  public CreateUser(userData: any): Observable<any> {
    return this.http.post(`${this.url}`, userData);
  }

  public UpdateUser(userId: number, userData: any): Observable<any> {
    return this.http.put(`${this.url}/${userId}`, userData);
  }

  public DeleteUser(userId: number): Observable<any> {
    return this.http.delete(`${this.url}/${userId}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices:

  • Use RxJS operators for data transformation
  • Handle errors with catchError
  • Return Observables for async operations
  • Keep service methods focused and single-purpose

Service Providers and providedIn

Different ways to provide services:

// Application-wide singleton (recommended)
@Injectable({
  providedIn: 'root'
})
export class GlobalService { }

// Platform-wide singleton (shared across multiple apps)
@Injectable({
  providedIn: 'platform'
})
export class PlatformService { }

// Module-scoped service
@Injectable()
export class ModuleService { }

// Then provide in module
@NgModule({
  providers: [ModuleService]
})
export class FeatureModule { }

// Component-scoped service (new instance per component)
@Component({
  selector: 'app-component',
  providers: [ComponentService]
})
export class MyComponent { }
Enter fullscreen mode Exit fullscreen mode

providedIn Options

Option Scope Use Case
'root' Application-wide Most services (recommended)
'platform' Platform-wide Shared across multiple apps
'any' Module-scoped Lazy-loaded modules
Module reference Module-scoped Feature modules
Component providers Component-scoped Component-specific instances

Why providedIn: 'root' is Preferred:

  • Enables tree-shaking (removes unused services)
  • Creates singleton automatically
  • No need to add to module providers
  • Better performance and bundle size

Service Communication Patterns

Services can communicate using RxJS:

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class NotificationService {
  private notificationsSubject = new BehaviorSubject<any[]>([]);
  public notifications$ = this.notificationsSubject.asObservable();

  addNotification(notification: any): void {
    const current = this.notificationsSubject.value;
    this.notificationsSubject.next([...current, notification]);
  }

  removeNotification(id: string): void {
    const current = this.notificationsSubject.value;
    this.notificationsSubject.next(current.filter(n => n.id !== id));
  }

  clearNotifications(): void {
    this.notificationsSubject.next([]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Using BehaviorSubject for State Management

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class UserStateService {
  private userSubject = new BehaviorSubject<any>(null);
  public user$ = this.userSubject.asObservable();
  public isAuthenticated$ = this.user$.pipe(
    map(user => !!user)
  );

  setUser(user: any): void {
    this.userSubject.next(user);
  }

  getUser(): any {
    return this.userSubject.value;
  }

  clearUser(): void {
    this.userSubject.next(null);
  }
}
Enter fullscreen mode Exit fullscreen mode

Component Subscription

export class UserComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();
  user: any;
  isAuthenticated: boolean = false;

  constructor(private userStateService: UserStateService) {}

  ngOnInit(): void {
    this.userStateService.user$
      .pipe(takeUntil(this.destroy$))
      .subscribe(user => {
        this.user = user;
      });

    this.userStateService.isAuthenticated$
      .pipe(takeUntil(this.destroy$))
      .subscribe(isAuth => {
        this.isAuthenticated = isAuth;
      });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

Injecting Services into Other Services

Services can inject other services:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { LoggerService } from './logger.service';
import { CacheService } from './cache.service';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor(
    private http: HttpClient,
    private logger: LoggerService,
    private cache: CacheService
  ) {}

  getData(id: number): Observable<any> {
    const cached = this.cache.get(id);
    if (cached) {
      return of(cached);
    }

    return this.http.get(`/api/data/${id}`).pipe(
      tap(data => {
        this.cache.set(id, data);
        this.logger.log('Data fetched:', data);
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Service Factory Pattern

Create services with factory functions:

import { Injectable, InjectionToken, FactoryProvider } from '@angular/core';

export const API_CONFIG = new InjectionToken<string>('api.config');

export function apiConfigFactory(): string {
  return environment.production 
    ? 'https://api.production.com' 
    : 'https://api.dev.com';
}

export const API_CONFIG_PROVIDER: FactoryProvider = {
  provide: API_CONFIG,
  useFactory: apiConfigFactory
};

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  constructor(@Inject(API_CONFIG) private apiUrl: string) {
    console.log('API URL:', this.apiUrl);
  }
}
Enter fullscreen mode Exit fullscreen mode

Service Testing

Test services independently:

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],
      providers: [UserService]
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

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

  it('should fetch users', () => {
    const mockUsers = [{ id: 1, name: 'John' }];

    service.GetUser(1).subscribe(user => {
      expect(user).toEqual(mockUsers[0]);
    });

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

Common Service Patterns

1. Data Service Pattern

@Injectable({
  providedIn: 'root'
})
export class DataService {
  private cache = new Map<string, any>();

  constructor(private http: HttpClient) {}

  get<T>(url: string, useCache: boolean = true): Observable<T> {
    if (useCache && this.cache.has(url)) {
      return of(this.cache.get(url));
    }

    return this.http.get<T>(url).pipe(
      tap(data => {
        if (useCache) {
          this.cache.set(url, data);
        }
      })
    );
  }

  clearCache(): void {
    this.cache.clear();
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Utility Service Pattern

@Injectable({
  providedIn: 'root'
})
export class UtilityService {
  formatDate(date: Date): string {
    return date.toLocaleDateString();
  }

  formatCurrency(amount: number): string {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    }).format(amount);
  }

  generateId(): string {
    return Math.random().toString(36).substr(2, 9);
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Authentication Service Pattern

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private tokenSubject = new BehaviorSubject<string | null>(null);
  public token$ = this.tokenSubject.asObservable();

  constructor(
    private http: HttpClient,
    private router: Router
  ) {
    const token = localStorage.getItem('token');
    if (token) {
      this.tokenSubject.next(token);
    }
  }

  login(credentials: any): Observable<any> {
    return this.http.post('/api/auth/login', credentials).pipe(
      tap(response => {
        const token = response.token;
        localStorage.setItem('token', token);
        this.tokenSubject.next(token);
      })
    );
  }

  logout(): void {
    localStorage.removeItem('token');
    this.tokenSubject.next(null);
    this.router.navigate(['/login']);
  }

  isAuthenticated(): boolean {
    return !!this.tokenSubject.value;
  }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Use providedIn: 'root' - For application-wide singleton services
  2. Keep services focused - Single responsibility principle
  3. Use services for business logic - Not presentation logic
  4. Handle errors appropriately - In service methods
  5. Return Observables - For async operations
  6. Use RxJS operators - For data transformation
  7. Keep services stateless - When possible
  8. Use BehaviorSubject - For shared state management
  9. Document service methods - And their return types
  10. Test services independently - From components
  11. Inject dependencies - Through constructor
  12. Use TypeScript types - For better type safety
  13. Avoid circular dependencies - Between services
  14. Use lazy loading - For feature-specific services when appropriate

Common Patterns

Service Organization

src/
  services/
    core/
      auth.service.ts
      http.service.ts
      logger.service.ts
    features/
      business.service.ts
      user.service.ts
      site.service.ts
    shared/
      notification.service.ts
      utility.service.ts
Enter fullscreen mode Exit fullscreen mode

Service with Error Handling

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  constructor(
    private http: HttpClient,
    private errorHandler: ErrorHandlerService
  ) {}

  get<T>(url: string): Observable<T> {
    return this.http.get<T>(url).pipe(
      catchError(error => {
        this.errorHandler.handle(error);
        return throwError(() => error);
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Resources and Further Reading

Conclusion

Angular Services and Dependency Injection provide a powerful way to organize code, share business logic, and manage application state. By following these patterns, you can build maintainable, testable, and scalable Angular applications. Services are the foundation of clean architecture in Angular applications.

Key Takeaways:

  • Angular Services - Singleton classes for reusable business logic
  • Dependency Injection - Automatic service provisioning
  • providedIn: 'root' - Application-wide singleton (recommended)
  • Service Communication - Using RxJS Observables and Subjects
  • Service Patterns - Data services, utility services, authentication services
  • Testing - Services can be tested independently
  • Best Practices - Keep services focused, handle errors, use Observables

Whether you're building a simple dashboard or a complex enterprise application, Angular Services provide the foundation you need. They handle all the business logic while keeping your components clean and focused on presentation.


What's your experience with Angular Services and Dependency Injection? Share your tips and tricks in the comments below! πŸš€


πŸ’‘ Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.

If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.

Top comments (0)