Example of Module Level Service in Angular

Angular provides a robust dependency injection (DI) system that allows you to manage how and where your services are available. One of the most flexible features of this system is the ability to scope services to specific modules — creating module-level services.

This post explores what module-level services are, why they matter, and how to implement them effectively using Angular modules. We’ll dive deep into their structure, use cases, lifecycle behavior, and best practices to ensure scalable and maintainable applications.

Table of Contents

  1. Introduction to Angular Modules
  2. What Are Module-Level Services
  3. Difference Between Root-Level and Module-Level Services
  4. Why Use Module-Level Services
  5. Creating a Module-Level Service
  6. Providing a Service in an Angular Module
  7. Example: Feature Module with a Scoped Service
  8. How Angular Handles Module-Level Service Instances
  9. How to Inject a Module-Level Service in a Component
  10. When to Use Module-Level Services
  11. Lazy Loading and Service Scoping
  12. Module-Level vs Component-Level Services
  13. Common Use Cases for Module-Level Services
  14. Sharing State Across Components in a Module
  15. Testing Module-Level Services
  16. Pitfalls and Mistakes to Avoid
  17. Best Practices for Module-Level Service Design
  18. Real-World Example: Feature-Specific Business Logic
  19. Migrating from Root-Level to Module-Level Services
  20. Conclusion

1. Introduction to Angular Modules

Angular applications are organized using modules — logical containers that group related functionality together. Each Angular application starts with a root module (AppModule), and can include additional feature modules for modular design.

Modules help you:

  • Organize your codebase
  • Enable lazy loading
  • Control the scope of components, directives, and services

A module can contain components, directives, pipes, and services — all related to a particular feature or area of the application.


2. What Are Module-Level Services

A module-level service is a service whose availability is limited to a specific Angular module.
It means that Angular will create a separate instance of the service for that module, rather than sharing a single instance across the entire application.

You define a module-level service by registering it in the providers array of a specific module.

Example:

@NgModule({
  providers: [ModuleService]
})
export class FeatureModule { }

Here, ModuleService is only available inside FeatureModule.


3. Difference Between Root-Level and Module-Level Services

FeatureRoot-Level ServiceModule-Level Service
ScopeAvailable throughout the entire appAvailable only inside a specific module
InstanceSingle instance (singleton)Separate instance for each module that provides it
DeclarationprovidedIn: 'root' or registered in AppModuleRegistered in the providers array of a module
Use CaseGlobal logic (auth, user state, configuration)Feature-specific logic (module-scoped behavior)

When you register a service at the module level, Angular’s dependency injector will create a new instance for that module and any of its components.


4. Why Use Module-Level Services

Module-level services are useful for several reasons:

  1. Encapsulation of Logic
    They keep feature-specific logic isolated from other parts of the app.
  2. Avoid Global Pollution
    They prevent global scope clutter by avoiding unnecessary injection in unrelated areas.
  3. Lazy Loading Compatibility
    Each lazy-loaded module can have its own service instance — enabling separate state handling.
  4. Maintainability
    They make the code easier to manage by organizing features independently.
  5. Improved Testability
    Each module can be tested independently with its own service configuration.

5. Creating a Module-Level Service

To create a service in Angular, use the Angular CLI:

ng generate service module

This creates a file named module.service.ts:

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

@Injectable()
export class ModuleService {
  private featureData = 'Feature Module Data';

  getFeatureData() {
return this.featureData;
} setFeatureData(data: string) {
this.featureData = data;
} }

Notice that we did not use providedIn: 'root'.
This means the service won’t automatically be provided globally — it must be registered manually in a module.


6. Providing a Service in an Angular Module

Once you create the service, register it in the module where you want it to be available.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FeatureComponent } from './feature.component';
import { ModuleService } from './module.service';

@NgModule({
  declarations: [FeatureComponent],
  imports: [CommonModule],
  providers: [ModuleService]
})
export class FeatureModule { }

Here’s what’s happening:

  • The service ModuleService is provided only to this module.
  • Angular creates a new instance of this service when this module is loaded.

7. Example: Feature Module with a Scoped Service

Let’s build a complete working example to understand module-level service behavior.

Step 1: Create the Service

@Injectable()
export class ModuleService {
  private counter = 0;

  increment() {
this.counter++;
} getCount() {
return this.counter;
} }

Step 2: Create a Feature Module

@NgModule({
  providers: [ModuleService],
  declarations: [FeatureComponent],
  imports: [CommonModule]
})
export class FeatureModule { }

Step 3: Create a Feature Component

import { Component } from '@angular/core';
import { ModuleService } from './module.service';

@Component({
  selector: 'app-feature',
  template: `
<h3>Module Service Counter: {{ count }}</h3>
<button (click)="increment()">Increment</button>
` }) export class FeatureComponent { count = 0; constructor(private moduleService: ModuleService) {} increment() {
this.moduleService.increment();
this.count = this.moduleService.getCount();
} }

Each time the user clicks the button, the counter stored in ModuleService is updated.

If another module uses its own version of ModuleService, it will have a separate counter — confirming module-level scoping.


8. How Angular Handles Module-Level Service Instances

Angular’s dependency injector creates a new service instance for each module that declares it in providers.

  • If ModuleService is provided in FeatureModule, all components, directives, and pipes in that module share the same instance.
  • If another module imports FeatureModule, Angular doesn’t reinstantiate the service — it reuses the existing one only within the same injector hierarchy.

However, if a module is lazy-loaded, it will have its own injector, and thus its own instance of the service.


9. How to Inject a Module-Level Service in a Component

Any component, directive, or pipe declared inside the same module can inject the service:

constructor(private moduleService: ModuleService) {}

Angular automatically resolves the dependency based on the module injector.

If the component belongs to a different module, it won’t have access unless the module is imported and the service is re-provided or made global.


10. When to Use Module-Level Services

You should use module-level services when:

  • The service logic is relevant only to a specific feature module.
  • You want independent instances of the same service for different modules.
  • You are using lazy loading and want each lazy-loaded module to maintain its own service state.
  • You want to limit the scope of a service to avoid accidental global sharing.

Examples include:

  • Feature-specific caching
  • Module-level settings
  • Localized data management

11. Lazy Loading and Service Scoping

Lazy loading is one of Angular’s most powerful features, and it pairs perfectly with module-level services.

Each lazy-loaded module gets its own injector tree.
That means a service provided in a lazy-loaded module has a unique instance, separate from the rest of the application.

Example:

@NgModule({
  providers: [ModuleService]
})
export class OrdersModule {}

When OrdersModule is lazy-loaded, it gets its own ModuleService instance, distinct from any instance in the root or other modules.

This allows each lazy-loaded module to maintain isolated state, reducing side effects and improving modularity.


12. Module-Level vs Component-Level Services

Both approaches limit scope, but there’s an important difference:

ScopeDeclared InShared WithLifecycle
Component-LevelComponent decorator (providers)Only that component and its childrenDestroyed with the component
Module-LevelNgModule providers arrayAll components in the moduleExists while the module is active

Choose component-level services for highly localized logic, and module-level services for broader but still contained logic.


13. Common Use Cases for Module-Level Services

  1. Feature-Specific Business Logic
    Example: An order module that manages order creation, history, and tracking.
  2. State Management for Feature Modules
    Keep local state separate from other modules.
  3. Local Data Caching
    Cache data per module to avoid unnecessary API calls.
  4. Configurable Services
    Different modules can provide their own configuration or implementation of the same service.

14. Sharing State Across Components in a Module

With module-level services, you can easily share data across components in the same module.

Example:

@Injectable()
export class CounterService {
  private count = 0;

  increment() {
this.count++;
} getCount() {
return this.count;
} }
@Component({
  selector: 'app-a',
  template: <button (click)="increment()">Add</button>
})
export class ComponentA {
  constructor(private counterService: CounterService) {}
  increment() {
this.counterService.increment();
} }
@Component({
  selector: 'app-b',
  template: Count: {{ count }}
})
export class ComponentB implements OnInit {
  count = 0;
  constructor(private counterService: CounterService) {}
  ngOnInit() {
this.count = this.counterService.getCount();
} }

Both components share the same CounterService instance within their module — perfectly encapsulated.


15. Testing Module-Level Services

You can easily test module-level services by configuring the module’s providers in the testing module.

import { TestBed } from '@angular/core/testing';
import { ModuleService } from './module.service';
import { FeatureModule } from './feature.module';

describe('ModuleService', () => {
  let service: ModuleService;

  beforeEach(() => {
TestBed.configureTestingModule({
  imports: [FeatureModule]
});
service = TestBed.inject(ModuleService);
}); it('should be created', () => {
expect(service).toBeTruthy();
}); it('should store and retrieve feature data', () => {
service.setFeatureData('New Data');
expect(service.getFeatureData()).toBe('New Data');
}); });

Unit tests help ensure that each module’s services function correctly in isolation.


16. Pitfalls and Mistakes to Avoid

  • Using providedIn: 'root' accidentally makes the service global.
  • Registering the same service in multiple modules can lead to multiple instances and confusion.
  • Importing feature modules in the root module unnecessarily may remove lazy-loading benefits.
  • Using shared state across modules unintentionally by not scoping properly.

Always double-check the scope of your service to ensure it behaves as intended.


17. Best Practices for Module-Level Service Design

  1. Define service providers explicitly in feature modules.
  2. Avoid circular dependencies between modules.
  3. Keep services cohesive — one responsibility per service.
  4. Use interfaces for consistency when multiple modules share a similar service contract.
  5. Test each service in isolation to confirm independent state behavior.
  6. Document service scope for clarity among developers.

18. Real-World Example: Feature-Specific Business Logic

Consider an e-commerce application with multiple modules:

  • ProductsModule
  • CartModule
  • OrdersModule

Each module may require its own logic for managing local state. For instance, the OrdersModule could have an OrderService:

@Injectable()
export class OrderService {
  private orders: any[] = [];

  addOrder(order: any) {
this.orders.push(order);
} getOrders() {
return this.orders;
} }

Registered in OrdersModule:

@NgModule({
  providers: [OrderService]
})
export class OrdersModule { }

This service is only available to components inside the OrdersModule, ensuring that order-related logic does not leak into unrelated parts of the application.


19. Migrating from Root-Level to Module-Level Services

If you’ve previously provided a service globally but want to scope it to a module:

  1. Remove providedIn: 'root' from the service.
  2. Register it in the providers array of the target module.
  3. Verify that only module-specific components use it.
  4. Test for unwanted cross-module dependencies.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *