Service Basics


Service Basics Interview with follow-up questions

1. What is a service in Angular?

A service is a plain TypeScript class that holds reusable logic or state not tied to a single view — data access, business logic, caching, logging, or shared state. Keeping this out of components follows separation of concerns and makes the logic reusable and testable.

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  getUser(id: string) { return this.http.get(`/api/users/${id}`); }
}

You mark it @Injectable() and register it — almost always with providedIn: 'root', which creates a tree-shakable, app-wide singleton — then inject it where needed via inject() or the constructor.

A modern point interviewers appreciate: services are now the natural home for signal-based state — a service can expose signal()/computed() values that components read reactively, which is the idiomatic way to share state across an app under zoneless change detection.

↑ Back to top

Follow-up 1

How do you create a service in Angular?

To create a service in Angular, you can use the Angular CLI command ng generate service serviceName. This will generate a new service file with the given name in the src/app directory. Alternatively, you can manually create a new TypeScript file and define a class that implements the desired functionality. To make the service available for dependency injection, you need to add the @Injectable() decorator to the service class.

Follow-up 2

What is the use of @Injectable decorator in a service?

The @Injectable() decorator is used to make a service class eligible for dependency injection. It marks the class as a provider that can be injected into other components or services. When a service is decorated with @Injectable(), Angular's dependency injection system can create instances of the service and inject them into the constructor of other classes that depend on it.

Follow-up 3

Can you explain the singleton nature of services in Angular?

In Angular, services are singletons by default. This means that Angular creates only one instance of a service class and shares it across all components that depend on it. When a service is injected into multiple components, they all receive the same instance of the service. This ensures that the data and state managed by the service are consistent across the application.

Follow-up 4

How do you use a service in a component?

To use a service in a component, you need to inject the service into the component's constructor. This is done using the dependency injection mechanism provided by Angular. Once the service is injected, you can access its properties and methods within the component. Here is an example of injecting and using a service in a component:

import { Component } from '@angular/core';
import { MyService } from './my.service';

@Component({
  selector: 'app-my-component',
  template: `<h1>{{ data }}</h1>`
})
export class MyComponent {
  data: string;

  constructor(private myService: MyService) {
    this.data = myService.getData();
  }
}

2. How does Angular service help in sharing data?

Because a service registered with providedIn: 'root' is a singleton, every component that injects it receives the same instance — so it's a natural shared container. One component writes to the service, another reads from it, and they stay in sync without being directly coupled.

@Injectable({ providedIn: 'root' })
export class CartService {
  private _items = signal([]);
  readonly items = this._items.asReadonly();
  add(item: Item) { this._items.update(list =&gt; [...list, item]); }
}

The modern best-practice answer: expose the shared state as a signal (or a computed) so consuming components update automatically and efficiently. The older pattern used an RxJS BehaviorSubject exposed as an Observable — still valid, but signals are now preferred for synchronous shared state and play nicely with zoneless change detection. This service-based approach is also how you communicate between unrelated/sibling components that can't use inputs/outputs.

↑ Back to top

Follow-up 1

Can you give an example of data sharing using a service?

Sure! Here's an example of how data can be shared using an Angular service:

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

@Injectable({
  providedIn: 'root'
})
export class DataService {
  sharedData: string;

  constructor() {
    this.sharedData = 'Hello, World!';
  }
}

In this example, we have a DataService that has a sharedData property. This property can be accessed and modified by any component that injects the DataService.

Follow-up 2

What are the advantages of using a service for data sharing?

Using a service for data sharing in Angular has several advantages:

  1. Centralized data management: Services provide a centralized location to store and manage shared data, making it easier to maintain and update.
  2. Reusability: Services can be injected into multiple components, allowing the same data to be shared across different parts of the application.
  3. Separation of concerns: By separating the data management logic into a service, components can focus on their own responsibilities and rely on the service for data sharing.
  4. Dependency injection: Services can be easily injected into components using Angular's dependency injection system, making it straightforward to access shared data.

Follow-up 3

How does a service maintain state in Angular application?

Services in Angular can maintain state by storing data as properties within the service class. When a component injects the service, it can access and modify these properties, effectively maintaining the state of the data. Since services are singleton instances in Angular, the state of the data will be preserved as long as the service instance exists.

Follow-up 4

What happens to the data in a service when the application is refreshed?

When the application is refreshed, the entire Angular application is reloaded, including all services. This means that the data stored in a service will be reset to its initial state. If you want to persist data across page refreshes, you can consider using other techniques such as local storage or server-side storage.

3. What is the difference between a service and a factory in Angular?

This is a bit of a trick question for 2026: "service vs factory" is AngularJS (1.x) terminology. AngularJS had multiple ways to register a provider — .service() (instantiated with new), .factory() (a function returning an object), .provider(), .value(), .constant() — and candidates were expected to know the differences.

In modern Angular there is no separate "factory" recipe. You write an ordinary @Injectable() class and register it as a provider. The closest equivalent to AngularJS's factory is the useFactory provider option, used when a dependency needs custom construction logic or runtime configuration:

{ provide: API_CLIENT, useFactory: () =&gt; new ApiClient(inject(Config).baseUrl) }

So the correct framing: in Angular, prefer a normal injectable service; reach for useFactory only when you must compute the instance (e.g. choose an implementation at runtime or pass dynamic constructor args). If an interviewer asks this expecting the AngularJS answer, it's worth noting the terminology is legacy.

↑ Back to top

Follow-up 1

Can you explain with an example?

Sure! Here's an example to illustrate the difference between a service and a factory in Angular:

// Service example

app.service('userService', function() {
    this.users = ['John', 'Jane', 'Bob'];
    this.getUsers = function() {
        return this.users;
    };
});

// Factory example

app.factory('userFactory', function() {
    var users = ['John', 'Jane', 'Bob'];
    return {
        getUsers: function() {
            return users;
        }
    };
});

// Usage in a controller

app.controller('UserController', function(userService, userFactory) {
    var serviceUsers = userService.getUsers();
    var factoryUsers = userFactory.getUsers();
    console.log(serviceUsers); // Output: ['John', 'Jane', 'Bob']
    console.log(factoryUsers); // Output: ['John', 'Jane', 'Bob']
});

In this example, the userService and userFactory both provide a getUsers method that returns an array of users. The main difference is how the objects are created and shared. The userService is a singleton object that is created once and shared across the application, while the userFactory creates a new object every time it is injected into a component or service.

Follow-up 2

When would you use a service over a factory?

You would use a service over a factory in the following scenarios:

  • Singleton object: If you need a single instance of an object that is shared across the application, you should use a service. Services are instantiated with the new keyword and are registered with the Angular dependency injection system as singletons.

  • Dependency injection: If you want to inject the object into other components or services using Angular's dependency injection system, you should use a service. Services are registered with the dependency injection system and can be easily injected into other components or services.

  • Common functionality or data: If you have common functionality or data that needs to be shared between different components, you should use a service. Services provide a way to encapsulate and share functionality or data across the application.

Follow-up 3

What are the limitations of using a service?

There are a few limitations of using a service in Angular:

  • Singleton object: Services are instantiated with the new keyword and are registered as singletons with the Angular dependency injection system. This means that there is only one instance of the service throughout the application. If you need multiple instances of an object, a service may not be suitable.

  • Limited control over object creation: Services are created and managed by the Angular dependency injection system. This limits your control over the creation and initialization of objects. If you need more control over object creation, you may need to use a factory instead.

  • Limited flexibility: Services have limited flexibility compared to factories. They cannot return custom objects or perform complex initialization logic. If you need more flexibility in creating objects, a factory may be a better choice.

Follow-up 4

What are the limitations of using a factory?

There are a few limitations of using a factory in Angular:

  • More complex syntax: Factories require you to write a function that returns an object. This can make the syntax more complex compared to services.

  • No automatic dependency injection: Unlike services, factories do not automatically receive dependencies from the Angular dependency injection system. You need to explicitly specify the dependencies when creating the factory function.

  • No built-in singleton behavior: Factories do not have built-in singleton behavior like services. If you need a singleton object, you need to implement the singleton pattern manually in your factory function.

Despite these limitations, factories provide more flexibility and control over object creation compared to services, which can be beneficial in certain scenarios.

4. How do you handle errors in an Angular service?

Most service errors come from async/HTTP work, so you handle them in the RxJS pipeline with the catchError operator — log, transform, or recover (e.g. return a fallback with of(...)), and rethrow with throwError when the caller should know:

getUser(id: string) {
  return this.http.get(`/api/users/${id}`).pipe(
    retry({ count: 2, delay: 1000 }),
    catchError(err =&gt; {
      this.logger.error(err);
      return throwError(() =&gt; new Error('Failed to load user'));
    })
  );
}

Points that round out a strong answer: use retry for transient failures; centralize cross-cutting handling (auth, 5xx, toasts) in a functional HTTP interceptor with withInterceptors() rather than repeating catchError everywhere; and register a global ErrorHandler for uncaught errors. If you've adopted the newer httpResource/resource() APIs, they expose an error signal you can render directly. The key principle: fail gracefully and surface a meaningful message, don't swallow errors silently.

↑ Back to top

Follow-up 1

Can you give an example of error handling in a service?

Sure! Here's an example of how you can handle errors in an Angular service:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

@Injectable()
export class MyService {
  constructor(private http: HttpClient) {}

  getData() {
    return this.http.get('https://api.example.com/data')
      .pipe(
        catchError(error =&gt; {
          console.error('An error occurred:', error);
          return throwError('Something went wrong. Please try again later.');
        })
      );
  }
}

In this example, the getData method makes an HTTP request to retrieve data from an API. If an error occurs during the request, the catchError operator is used to catch the error and handle it. The error is logged to the console, and a custom error message is returned using the throwError function.

Follow-up 2

What is the role of RxJS in error handling?

RxJS plays a crucial role in error handling in Angular services. It provides operators like catchError, retry, and retryWhen that allow you to handle and recover from errors in a flexible and reactive way. These operators can be used to catch errors, retry failed requests, and implement more complex error handling strategies.

Follow-up 3

How do you retry a failed HTTP request in a service?

To retry a failed HTTP request in an Angular service, you can use the retry operator from RxJS. This operator allows you to automatically retry the request a specified number of times when an error occurs. Here's an example:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, retry } from 'rxjs/operators';
import { throwError } from 'rxjs';

@Injectable()
export class MyService {
  constructor(private http: HttpClient) {}

  getData() {
    return this.http.get('https://api.example.com/data')
      .pipe(
        retry(3),
        catchError(error =&gt; {
          console.error('An error occurred:', error);
          return throwError('Something went wrong. Please try again later.');
        })
      );
  }
}

In this example, the getData method retries the HTTP request up to 3 times if an error occurs. If the request still fails after the specified number of retries, the error is caught and handled using the catchError operator.

Follow-up 4

How do you handle multiple errors in a service?

When handling multiple errors in an Angular service, you can use the catchError operator along with other RxJS operators like forkJoin or combineLatest. These operators allow you to handle multiple observables and their errors in a coordinated way. Here's an example:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, forkJoin, throwError } from 'rxjs';

@Injectable()
export class MyService {
  constructor(private http: HttpClient) {}

  getData() {
    const request1 = this.http.get('https://api.example.com/data1');
    const request2 = this.http.get('https://api.example.com/data2');

    return forkJoin([request1, request2])
      .pipe(
        catchError(errors =&gt; {
          console.error('An error occurred:', errors);
          return throwError('Something went wrong. Please try again later.');
        })
      );
  }
}

In this example, the getData method makes two HTTP requests using the forkJoin operator. If any of the requests fail, the catchError operator is used to catch the errors and handle them. The errors are logged to the console, and a custom error message is returned using the throwError function.

5. How do you test an Angular service?

Services are the easiest things to unit-test because they're plain classes. You configure a TestBed, inject the service, and assert on its behavior:

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

  beforeEach(() =&gt; {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() =&gt; httpMock.verify());

  it('fetches a user', () =&gt; {
    service.getUser('1').subscribe(u =&gt; expect(u.name).toBe('Ada'));
    httpMock.expectOne('/api/users/1').flush({ name: 'Ada' });
  });
});

Two current notes: use provideHttpClient() + provideHttpClientTesting() with HttpTestingController to mock HTTP (the old HttpClientTestingModule is the legacy equivalent); and for pure logic with no Angular deps, you can often skip TestBed and just new the service with mock dependencies. Also, as of Angular 21 the default test runner is Vitest (Karma is deprecated), though the TestBed/spec API you write looks the same.

↑ Back to top

Follow-up 1

What are the steps to test a service?

The steps to test an Angular service are as follows:

  1. Import the necessary dependencies for testing, such as TestBed and the service itself.
  2. Configure the testing module by calling TestBed.configureTestingModule() and providing any necessary dependencies or providers.
  3. Use TestBed.createComponent() to create an instance of the component that uses the service.
  4. Access the service instance using fixture.debugElement.injector.get() or TestBed.get().
  5. Write test cases to verify the behavior of the service methods and properties.
  6. Use fixture.detectChanges() to trigger change detection if necessary.
  7. Assert the expected results using Jasmine's expect() and other assertion methods.
  8. Clean up any resources or reset the state after each test using fixture.destroy() or other appropriate methods.

Here is an example of testing an Angular service:

import { TestBed } from '@angular/core/testing';

import { MyService } from './my.service';

describe('MyService', () =&gt; {
  let service: MyService;

  beforeEach(() =&gt; {
    TestBed.configureTestingModule({
      providers: [MyService]
    });
    service = TestBed.inject(MyService);
  });

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

  it('should return the correct value', () =&gt; {
    const result = service.getValue();
    expect(result).toBe('Hello, World!');
  });
});

Follow-up 2

What is the role of TestBed in testing a service?

In Angular testing, TestBed is a utility class provided by the @angular/core/testing package. It is used to configure and create a testing module, which is an Angular module specifically designed for testing purposes.

The role of TestBed in testing a service is to:

  • Provide a way to configure the testing module by calling TestBed.configureTestingModule() and specifying the necessary dependencies or providers for the service being tested.
  • Create an instance of the component that uses the service using TestBed.createComponent().
  • Access the service instance using fixture.debugElement.injector.get() or TestBed.get().

By using TestBed, you can set up the necessary environment for testing a service and easily access its instance for testing purposes.

Follow-up 3

How do you mock dependencies in a service test?

To mock dependencies in an Angular service test, you can use the TestBed.configureTestingModule() method to provide mock implementations of the dependencies.

Here is an example of how to mock a dependency in a service test:

import { TestBed } from '@angular/core/testing';

import { MyService } from './my.service';
import { DependencyService } from './dependency.service';

describe('MyService', () =&gt; {
  let service: MyService;
  let mockDependencyService: jasmine.SpyObj;

  beforeEach(() =&gt; {
    mockDependencyService = jasmine.createSpyObj('DependencyService', ['getValue']);
    mockDependencyService.getValue.and.returnValue('Mocked Value');

    TestBed.configureTestingModule({
      providers: [
        MyService,
        { provide: DependencyService, useValue: mockDependencyService }
      ]
    });

    service = TestBed.inject(MyService);
  });

  it('should use the mocked dependency', () =&gt; {
    const result = service.getValueFromDependency();
    expect(result).toBe('Mocked Value');
    expect(mockDependencyService.getValue).toHaveBeenCalled();
  });
});

Follow-up 4

How do you test a service method that returns an Observable?

To test a service method that returns an Observable, you can use the TestBed.configureTestingModule() method to provide a mock implementation of the Observable using the of() function from the rxjs library.

Here is an example of how to test a service method that returns an Observable:

import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';

import { MyService } from './my.service';

describe('MyService', () =&gt; {
  let service: MyService;

  beforeEach(() =&gt; {
    TestBed.configureTestingModule({
      providers: [MyService]
    });

    service = TestBed.inject(MyService);
  });

  it('should return an Observable with the correct value', () =&gt; {
    const result$ = service.getData();

    result$.subscribe(result =&gt; {
      expect(result).toBe('Hello, World!');
    });

    expect(result$).toBeObservable(of('Hello, World!'));
  });
});

6. What are Observables and how does Angular use RxJS?

An Observable is a lazy, push-based stream of zero or more values over time, which you transform with operators (map, filter, switchMap, debounceTime, catchError) in a pipe() and consume by subscribing. Angular uses RxJS throughout: HttpClient returns Observables, the Router exposes paramMap/events, reactive forms expose valueChanges/statusChanges, and EventEmitter is Observable-based.

this.search.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(q =&gt; this.api.search(q))
).subscribe(results =&gt; /* … */);

Key points interviewers probe: Observables are lazy (nothing runs until subscribe) and multicast vs unicast (Subjects vs plain Observables); always avoid leaks by using the async pipe or takeUntilDestroyed(); and know switchMap vs mergeMap/concatMap/exhaustMap for flattening. The modern nuance: use signals for synchronous state and RxJS for async streams, bridging with toSignal().

↑ Back to top

Live mock interview

Mock interview: Service Basics

Intermediate ~5 min Your own free AI key

Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.