Services are an integral part of Angular applications, but should they be? Why do we call an object a service, and what are the consequences of doing so? In this article, I aim to explore the topic of naming objects, discuss the risks of failing to define their responsibilities precisely, and how to avoid this.
Shortly after I conceived the idea for this article, an RFC concerning Angular style guides was published. Among its recommendations was to stop using the „service” suffix in class and file names. As the author explains: “The term 'service’ (…) does not add any meaningful information to explain what a class does.”
After heated discussion, it was decided to leave this matter to developers and not include suffix-related rules in the style guides. However, official documentation and examples will no longer use such suffixes. Link
But let’s not get ahead of ourselves and start by examining the current conventions for naming objects in Angular.
What Types of Classes and Objects Exist in a Typical Angular Application?
When designing an Angular application, we rely on several fundamental types of objects, such as directives (including components), pipes, guards, resolvers, and interceptors. These objects have more or less clearly defined roles. Simply put: pipes transform data within templates, guards ensure some routing paths are accessible only under specific conditions, resolvers provide data for views before they are displayed, and interceptors modify outgoing and incoming requests.
Components, on the other hand, are the foundational building blocks of every application, defining both the view and its logic. Since the definition of a component is quite broad, we often classify components as „smart” or „dumb” for better clarity about their responsibilities.
And What About Services?
Defining the role of service is not as straightforward. Services are mainly associated with API communication, implementing business logic, or managing state — essentially, delegating tasks from components and sharing data and logic between them.
In practice, we often call any object or class wrapped in the @Injectable decorator and bearing the „Service” suffix a service. The @Injectable decorator makes the class available for providing and dependency injection. So by naming an object as a service, we are merely stating that it is a dependency.
However, you’ve likely encountered classes available for providing that we don’t call services — for example, interceptors, guards, or resolvers. Thus, we label as services those dependencies we have not given a specific name.
These facts may lead us to think that the term „service” is a convenient shortcut Angular developers use to avoid clearly defining an object’s responsibility and name.
Side Note: The Functional Approach
It’s worth mentioning that newer Angular versions introduce a functional approach to defining guards and resolvers. However, I continue to refer to them as classes and objects in this context. The functional approach does not affect considerations of object responsibility, so I will stick to object-oriented terminology.
Delegating Responsibilities
As mentioned earlier, we delegate responsibilities from components to services. Delegation is crucial for improving code quality. However, it’s important to carefully consider whom we delegate those responsibilities to. Without a clearly defined purpose for the object we’re delegating to, code can quickly become unmanageable. Thus, we should delegate tasks to classes and objects with precisely defined roles. A well-chosen name helps clarify an object’s role and how other objects should interact with it.
Let’s now consider how object-oriented programming principles can help us choose better names.
Where to Find the Right Name? Let’s Turn to OOP!
Object-oriented programming (OOP) goes beyond defining what classes and objects are or what polymorphism and encapsulation mean. It also offers best practices for working with these concepts (e.g., SOLID principles) and design patterns, which provide solutions to common problems.
Design patterns, in particular, can help us define an object’s name and responsibility. Let’s take a closer look at them.
What Are Design Patterns?
Simply put, they are methods for solving commonly encountered problems in software systems. By „problems,” I don’t mean bugs but challenges that software aims to solve. For example, in frontend development, abstracting repetitive operations like adding headers to requests is a problem. A solution is to use an object with a specific purpose and implementation — an interceptor. Each type of object mentioned earlier can also be described as a design pattern.
To use design patterns more consciously, let me briefly outline their history and formal definition.
History of Design Patterns
The concept of design patterns in computer science was proposed by „Gang of Four” in their iconic book Design Patterns: Elements of Reusable Object-Oriented Software. These patterns are universal solutions to common design problems and have inspired developers for decades to write flexible, maintainable code.
Definition of a Pattern
To describe a pattern clearly, we should include:
- A name
- A description of the problem it solves
- The solution (what classes, objects, interfaces, and dependencies it involves, typically with precise naming)
- Consequences of applying it
Returning to Angular, how can we apply these patterns effectively?
Domain-Specific Patterns
Besides general design patterns, frameworks like Angular introduce their own concepts, such as components, pipes, or directives, to guide application development. By adhering to Angular’s patterns and common design patterns, we’re halfway to success.
The other half involves identifying repetitive challenges specific to our application and defining patterns to address them. Although Angular is an opinionated framework, building a well-thought-out application will still involve addressing domain-specific problems systematically.
Let’s explore some of such practical challenges deserving systematic approaches.
Examples of Issues Requiring Methodological Approach
API Communication
This is a typical task we delegate to services to abstract request implementation details and enable sharing. By limiting an object’s responsibility solely to this (excluding data storage, error handling, or other side effects), we create a clear, single-responsibility object I like to call an API Facade. For example, a class serving as a facade for product API interactions might be called ProductsApiFacade.
State Management
I’m a fan of state management libraries, especially the NgRx Signal Store recently. In its documentation, the file defining it isn’t called a „service” but a „store.” Similarly, when using traditional Redux-based stores, each type of object has its own name — reducer, selector, action, or effect — and files are named accordingly. So why, when implementing custom state management (e.g., a service with a Subject), do we sometimes still call it a „service” instead of a „store”?
Regardless of the implementation, we should name the class (and file) with „store,” e.g., ProductsStore. Then, strictly ensure that the store object’s responsibility is limited to being a store.
Extracting Component Logic: Presenter as an Anti-Pattern
Overgrown components are hard to maintain, so developers often delegate some of their logic to helper objects—leading some to adopt the presenter pattern from MVP. One of the most popular sources for this approach is Lars Nielsen’s series on MVP in Angular.
I see two significant issues with this approach:
- Angular is component-based, not MVP-based. While the presenter in Nielsen’s article is defined as an object handling complex presentation logic, this doesn’t align with the original MVP definition. Misusing a common term in a new context can be confusing.
- The term “presenter” doesn’t precisely define the object’s responsibility, making it comparable to a service, though tightly coupled with the presentational component.
Instead of delegating all “complex logic” to a “presenter,” delegate specific tasks (e.g., form handling) to objects designed for those tasks.
How to Implement Standards?
Once we identify problems and propose solutions, we need to ensure the entire team adopts them.
- Documentation: Maintaining project pattern documentation is crucial. While it may be tedious, it organizes concepts, facilitates onboarding, and supports code reviews.
- Communication: Regular team meetings to discuss project approaches, pain points, and improvement proposals are invaluable.
- Consistency: Ensure adherence to standards through code reviews. If team members struggle, revisit the definitions to ensure clarity and accessibility.
Conclusion
In this article, I’ve argued for replacing services with objects that have well-defined purposes and precise names. I’ve emphasized that vague object names can lead to loosely defined responsibilities and proposed leveraging common design patterns and creating project-specific patterns to address domain challenges. Implementing these patterns effectively requires documentation, communication, and consistent enforcement of agreed-upon standards.