22 Apr 2025
3 min

Lightweight Port and Adapter Pattern for NGRX Signal Store

What is ngrx Signal Store?

The NgRx Signal Store is a state management solution for Angular applications that is built on top of Angular’s reactive signals, which were introduced in Angular 16. NgRx Signal Store is designed to be a more efficient and simpler alternative to the traditional NgRx Store. It integrates Angular signals with the NgRx ecosystem, allowing developers to manage application state in a way that is both reactive and easier to reason about compared to the traditional NgRx store, which relies heavily on observables and actions.

more about signals on angular.love:

  • https://angular.love/en/angular-signals-a-new-feature-in-angular-16

more about the signal store on angular.love:

  • https://angular.love/en/breakthrough-in-state-management-discover-the-simplicity-of-signal-store-part-1

What is Hexagonal Architecture (Ports and Adapters Pattern)?

The Hexagonal architecture (Ports and Adapters) isolates core business logic from external systems by using interfaces (ports) to define interactions and adapters to handle implementations. This separation enhances modularity, testability, and flexibility, allowing the core logic to remain independent of infrastructure changes, making the application easier to maintain and adapt.

The Hexagon Pattern in the Angular framework, when combined with Angular’s powerful Dependency Injection (DI) mechanism, provides an elegant way to manage and simplify complex interactions within an application. It allows developers to encapsulate intricate logic and expose a simplified API to the rest of the application, enhancing modularity, maintainability, and testability.

More about dependency injection on angular.love:
https://angular.love/en/dependency-injection-in-angular-everything-you-need-to-know more about Hexagon Architecture on angular.love:

  • https://angular.love/ports-and-adapters-vs-hexagonal-architecture-is-it-the-same-pattern

The power of signal store type inference

NgRx SignalStore is a state management solution that leverages TypeScript’s type inference to provide a more powerful and type-safe experience for managing state in Angular applications without the need to explicitly define types.  This reliance on type inference reduces boilerplate code, enhances code readability, and ensures type safety across the application, making it easier to maintain and refactor while preventing runtime errors due to type mismatches. The result is a more intuitive API that supports modern Angular development practices with minimal effort and maximum reliability.

Lightweight port and adapter implementation for ngrx Signal Store 

Combining NgRx SignalStore with the Hexagonal Architecture (Ports and Adapter pattern) offers a powerful approach to managing state in Angular applications while preserving the type inference benefits provided by TypeScript. This integration allows us to leverage the flexibility and modularity of the hexagonal pattern, creating clear boundaries between business logic and application infrastructure without introducing unnecessary complexity or additional code. By adopting this approach, we can gain the advantages of hexagonal architecture, such as improved testability and easier code maintenance, while maintaining the simplicity and minimalism offered by NgRx SignalStore.

Usually, in TypeScript, the implementation of an adapter is based on the interface implementation mechanism, but this time, we will use the 'satisfies’ operator:

import { Type } from "@angular/core";

export interface Fruit {
  id: string;
  name: string;
}

export interface FruitService {
  fruits: Signal<Fruit[]>;
  loadFruits(): void;
}

const FruitServiceAdapter = signalStore(
  withState({ fruits: [] as Fruit[] }),
  withMethods((store) => {
    return {
      loadFruits: async () => {
        const fruits = await fetch('https://api.example.com/fruits').then(
          (res) => res.json()
        );
        patchState(store, {
          fruits,
        });
      },
    };
  })
) satisfies Type<FruitService>;

The ‘Satisfies’ operator forces the value to match the given type but does not change the type of the value (e.g., resulting from the type inference mechanism). This way, we don’t overwrite the inferred type for the signal store, but we ensure that the port is implemented correctly.

We can now prepare convenient utilities to inject the adapter underneath the port:

const fruitServiceInjectionToken = new InjectionToken<FruitsService>(
  'fruits-service'
);

export function provideFruitService(): Provider {
  return {
    provide: fruitServiceInjectionToken,
    useClass: FruitServiceAdapter,
  };
}

export function injectFruitService(): FruitsService {
  return inject(fruitServiceInjectionToken);
}

And this is what it looks like to use inside a component or another service:

@Component({
  selector: 'app-root',
  standalone: true,
  template: ``,
  providers: [provideFruitService()],
})
export class App {
  private fruitService = injectFruitService();

  constructor() {
    this.fruitService.loadFruits();
  }
}

Stackblitz: 

https://stackblitz.com/edit/stackblitz-starters-pbmwtt?file=src%2Ffruit.service.ts

We can also simplify the injection utilities using abstract classes. That approach was described in angular.love article about Ports and Adapters:

  • https://angular.love/ports-and-adapters-vs-hexagonal-architecture-is-it-the-same-pattern 

Final Thoughts:

Combining NgRx SignalStore with the Hexagonal Architecture offers a modern, type-safe approach to state management in Angular applications. By leveraging TypeScript’s powerful type inference, we can maintain the simplicity and minimalism of SignalStore while gaining the modularity and testability benefits of the hexagon pattern. This approach not only reduces boilerplate and improves code readability but also ensures robust application architecture with a clear separation of concerns. With minimal additional code, you can implement a clean, maintainable, and scalable state management solution that embraces both the reactive paradigm and the principles of clean architecture. This synergy makes it easier for developers to build and maintain complex applications with confidence and efficiency.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.