Commonly used design patterns in Angular
In software development, regardless of where you work, the programming language used, and the application being built, change will always be your friend. For inexperienced developers their focus is on writing code that would enable their software to adapt to changes in the short term without considering its ability to be reused, extended, and maintained in future changes.
This approach leads to their spending much more time on the maintenance phase. In the worst-case scenario, they will have to completely rebuild the software project they are handling from the beginning. That's the reason why Design Patterns was born to save the world.
Design patterns definition and classifications
Design patterns are commonly used solutions to problems that occur during software design. Rather than providing a complete design to be implemented directly, they serve as templates demonstrating effective approaches to solving the software design problem.
The popularity of design patterns can be traced back to the publication of "Design Patterns: Elements of Reusable Object-Oriented Software" by Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in 1994. Initially, there were 23 design patterns introduced in the book. However, as the software engineering field has continued to evolve, new design patterns have been discovered, and today, there are hundreds or even thousands of design patterns being utilized by software engineers across various areas of development (such as backend, web, mobile apps), programming languages, frameworks, and more.
Design patterns are categorized into different areas, such as:
- Gang of Four design patterns offer reusable solutions to object-oriented programming problems. These patterns are divided into Creational Patterns, Structural Patterns, and Behavioral Patterns. For more information on Gang of Four design patterns, click here.
- Architectural design patterns provide reusable solutions to common problems in software architecture. In web development, there are various architectural design patterns, such as MV* (MVC, MVP, MVVM) patterns, component-based patterns, micro-frontend, atomic design systems, Flux, Redux, and many more. You can find a list of architectural patterns here.
- Rendering design patterns, which offer rendering strategies in web applications. These patterns address whether the web application should be rendered on the web server, build server, on the Edge, or directly on the client. They also consider whether it should be rendered simultaneously, partially, or progressively. To learn more about rendering design patterns, click here.
- Angular component design patterns provide strategies for sharing data between components in Angular applications. For more information on this topic, click here.
These are just a few of the many design patterns available. The next section will discuss how Halodoc implements design patterns in Angular applications.
The implementation of the Angular design pattern at Halodoc
In previous blog posts, we have covered several design patterns used by Halodoc, including:
- Architectural design patterns, we have discussed Microfrontend Architecture and Atomic Design System
- Rendering design patterns, we have discussed Server-side Rendering and Pre-rendering
- Performance design patterns, we have discussed Caching API data using Transfer State
And now, in this blog, we will discuss how we implement the Gang of Four design pattern with Angular way.
The Singleton pattern is a design pattern that limits a class's instantiation to only one object. In some situations, such as when using a service to store global data, using the same instance from anywhere is necessary. Angular has its approach to implementing this Singleton pattern. There are two methods to create a service as a singleton in Angular:
1. Make a service singleton throughout all modules
To ensure that the service is accessible to all modules, the
providedIn property of the
@Injectable() in the service file must be set to
"root", resulting in a singleton service. The following code illustrates this implementation:
By employing this approach, the
UserSessionService class will consistently provide a singular instance of the service, regardless of the number of times or locations in which it is instantiated across the entire application's modules.
2. Make the service singleton for a particular module
To make the service a singleton only within a specific module, it can be registered within the provider's section of that module, as demonstrated in the following code:
By implementing this approach, the
UserSessionService class will provide a singular instance of the service solely within the
HomeModule. However, if the
UserSessionService is provided in other modules, the instance of the service will not be the same as the instance in
The Factory pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. In Angular, the Factory pattern is often used to create different services or components based on some condition or input. In this case, we will explain the Factory pattern implemented using providers in Angular module.
In Halodoc, in one of the cases, we are using Factory Pattern for the TranslateLoader. As background, for some applications, we are using server-side rendering (SSR). The problem is translating loader implementation between client-side rendering (CSR) and server-side rendering (SSR) is different. So, in this case, we are using Factory Pattern to decide which TranslateLoader we will use. Here are the implementations:
With this implementation, the provider for the
TranslateLoader service uses the
translateLoaderFactory() function to create a concrete implementation of the
TranslateLoader interface based on the value of the platform. If our application is running on the server, then we will use
TranslateServerLoader, otherwise will use
Facade Pattern is a structural design pattern that provides a simplified interface to a complex system of classes, interfaces, and objects. In an Angular application, the Facade Design Pattern can be used to simplify the interactions between components and services.
Facade Design Pattern is often used to hide the complexities of a system behind a more straightforward, more user-friendly interface. This is useful when you want to provide a consistent interface to a complex system that is easier to use, test, and maintain. To implement the Facade Design Pattern in Angular, you can create a facade service that acts as a simplified interface to the complex system of services and components. This service can expose only the necessary functionality to the rest of the application while hiding the implementation details.
Let's take an example of our case in the shopping cart in our application. The shopping cart system may consist of multiple services and components, such as a cart service, a checkout component, a product component, and an order service. Instead of exposing all of these components and services directly to the rest of the application, we can create a facade service that provides a simplified interface to the cart system.
To illustrate the advantages of using the Facade Pattern, I would like to present an example of a scenario implemented both with and without this design pattern. This will clarify the benefits of utilizing the Facade Pattern.
By examining the diagrams provided above, it becomes apparent that in the absence of the Facade Pattern, the logic on the component side becomes more manageable, especially when multiple components share similar logic. Additionally, without the Facade Pattern, there is a risk of encountering duplicate code.
However, when utilizing the Facade Pattern, the complexity of the components is transferred to the facade. Additionally, with the inclusion of the component, we can focus solely on UI interaction rather than business logic. The code snippets below exemplify how the Facade Pattern can be implemented for a cart system use case.
In this example, the
CartFacade service acts as a simplified interface to the cart system. It exposes only the necessary functionality to the rest of the application, such as adding items to the cart, removing items from the cart, checking out, and getting products and points.
In this example, the
ProductDetailsComponent component displays the details of a product and provides two buttons to add the product to the cart and checkout.
The component injects the
CartFacade service and uses its
checkout() methods to add the product to the cart and checkout when the corresponding buttons are clicked.
By using the
CartFacade service in the component, we can simplify the interactions with the shopping cart system and provide a more user-friendly interface.
Anti-patterns in Angular
As mentioned earlier in this blog, design pattern is a reusable solution to a commonly occurring problem in software design. However, sometimes developers can make things more complicated by using design patterns inappropriately or excessively. These problematic usages of design patterns are known as "anti-patterns". The following are several examples of anti-patterns in Angular development:
- Overusing the Singleton Pattern: The Singleton pattern can be a valuable pattern in certain scenarios, but excessive use of it can result in tightly-coupled code that is challenging to maintain and test. In Angular, the overuse of
providedIn: 'root'can also bloat the main bundle size. To mitigate these issues, it's crucial to design Singletons to promote testability and assign them a single, well-defined responsibility.
- Misusing Observables: Observables are a powerful pattern in Angular for managing asynchronous data streams. However, it's possible to misuse or overuse them, resulting in unwanted complexity or poor performance. One common anti-pattern is to subscribe to observable multiple times, leading to redundant API calls. Another anti-pattern is to forget to unsubscribe from the observable, leading to memory leaks. To prevent these issues, consider using the async pipe to handle subscriptions and unsubscriptions automatically.
To avoid these anti-patterns, it's important to understand each design pattern's strengths and limitations and use them appropriately in Angular applications. It's also important to follow best practices for Angular development, such as using dependency injection, following the Single Responsibility Principle, and writing testable code. We have created a blog to discuss best practices and guidelines for web apps using Angular, and more detail can be seen here:
Design patterns offer practical solutions to recurring software design problems. Their usefulness has been proven over time, as they reduce development time and cost, increase application reliability, and enhance maintainability. As the software engineering field continues to evolve, new design patterns are being discovered and implemented across various areas of development, programming languages, and frameworks.
In this blog, we have discussed the implementation of the Gang of Four design pattern with Angular, specifically the Singleton, Factory, and Facade patterns. Applying these patterns can help enhance Angular applications' reusability, scalability, and maintainability. Nonetheless, it's crucial to understand the pros and cons of each design pattern to avoid any anti-patterns.
Additionally, we demonstrated the effectiveness of these design patterns in real-world scenarios by sharing how Halodoc uses them in our applications. Learning to use design patterns is a crucial skill for developers to master. It allows them to develop adaptable, maintainable, and extensible software solutions that adapt to future changes.
Scalability, reliability and maintainability are the three pillars that govern what we build at Halodoc Tech. We are actively looking for engineers at all levels and if solving hard problems with challenging requirements is your forte, please reach out to us with your resumé at firstname.lastname@example.org.
Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke. We connect 20,000+ doctors with patients in need through our Tele-consultation service. We partner with 3500+ pharmacies in 100+ cities to bring medicine to your doorstep. We've also partnered with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allow patients to book a doctor appointment inside our application. We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates Foundation, Singtel, UOB Ventures, Allianz, GoJek, Astra, Temasek and many more. We recently closed our Series C round and In total have raised around USD$180 million for our mission. Our team works tirelessly to make sure that we create the best healthcare solution personalised for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.