Ports and Adapters vs. Hexagonal Architecture: Same Pattern, Different Names?
“Hexagonal Architecture” centers on the concept of a core surrounded by multiple sides (hexagon is just a common metaphor—the actual number of sides is irrelevant) representing different external systems (adapters), with ports as their interfaces.

“Ports and Adapters” conveys the same simplified model in a more direct fashion and explicitly names the key elements: Ports (interfaces) and Adapters (implementations). This term is more often used when the hexagon visual isn't necessary.
Both Hexagonal Architecture and Ports and Adapters strive to isolate business logic from external systems through interfaces (ports) and implementations (adapters). They describe essentially the same architectural pattern and are frequently used synonymously.
Understanding the Ports and Adapters Pattern
Ports and Adapters was introduced by Alistair Cockburn (known for co-creating and signing the Agile Manifesto in 2001). The main objective is to keep an application's core business logic—the „heart” of the app—separated from external dependencies like databases, user interfaces, and third-party services.
At the architecture's heart sits the core business logic. This is where all essential business rules, domain models, and application services reside. The core remains independent of external systems, so business logic doesn't depend on infrastructure specifics like browser APIs, HTTP layers, or framework features. Keeping the core highly cohesive, it encapsulates what the application actually does. With no infrastructure dependencies, testing and maintenance become considerably easier.
Ports are abstract interfaces specifying how the core communicates with the outside world. For example, they might define application use cases like “createTodoItem” or “changeItemStatus”.
Adapters are concrete port implementations. They serve as the link between core business logic and external systems, translating external data formats, protocols, or requests into something the core can process.

Why and When to Apply Ports and Adapters?
This pattern aims to make software applications more modular, easier to maintain, and better equipped to handle change.
- Separation of Concerns: Keeping core business logic separate from external systems allows each application part to evolve on its own. Infrastructure or external service changes don't impact core logic, and vice versa.
- Modularity: The architecture promotes building modules that are straightforward to replace or upgrade. For instance, swapping a database adapter—like moving from a relational to a NoSQL database—can be done without altering core business logic.
- Testability: Because core business logic is isolated from external dependencies, you can test it independently with mock port implementations. This enables more reliable unit tests and simplifies bug detection.
- Flexibility: The architecture supports various ways to interact with the application. The same core logic can be accessed through a web interface, command-line tool, or external API by building different inbound adapters.
If you're encountering difficulties with any of the above aspects, implementing Ports and Adapters could be the right move.
Implementing Ports and Adapters (Hexagonal Architecture) in Angular
Luckily, putting Ports and Adapters into practice in Angular is quite straightforward, thanks to TypeScript itself and Angular's built-in Dependency Injection.
First, let's address the port implementation. As explained, it's basically an abstract interface, so we'll use a TypeScript interface for this. Like the rest of the core business logic, the Port itself should have zero dependencies on external infrastructure.
export interface FruitService {
getAllFruits(): Observable<Fruit[]>;
getFruitById(id: string): Observable<Fruit>;
}
Next, the adapter is a specific implementation of a port, so we can create a TypeScript class that implements the port:
@Injectable()
export class FruitServiceAdapter implements FruitService {
private readonly httpClient = inject(HttpClient);
getAllFruits(): Observable<Fruit[]> {
return this.httpClient.get<Fruit[]>('/fruits/all');
}
getFruitById(id: string): Observable<Fruit> {
return this.httpClient.get<Fruit>(`/fruits/${id}`);
}
}
To connect the port and the adapter, we can set up an injection token that stands for the port but actually provides the adapter underneath.
export const FRUIT_SERVICE = new InjectionToken<FruitService>('fruit-service');
@Component({
...
providers: [
{
provide: FRUIT_SERVICE,
useClass: FruitServiceAdapter,
},
],
})
export class App {
fruitService = inject(FRUIT_SERVICE);
}
Using the port as a generic type in the injection token ensures the injected service is automatically typed as a port.
Using Abstract Classes for Ports and Adapters in Angular
We can streamline the implementation above because TypeScript allows using an abstract class as a type, and, unlike an interface, it isn't removed during compilation.
Let's adapt our port to be an abstract class with abstract properties:
export abstract class FruitService {
abstract getAllFruits(): Observable<Fruit[]>;
abstract getFruitById(id: string): Observable<Fruit>;
}
The adapter implementation stays the same (in TypeScript, a class can implement another abstract class):
@Injectable()
export class FruitServiceAdapter implements FruitService { ... }
Finally, we can use the abstract class itself as an injection token, avoiding the need to define a new token explicitly:
@Component({
...
providers: [
{
provide: FruitService,
useClass: FruitServiceAdapter,
},
],
})
export class App {
fruitService = inject(FruitService);
}
Stackblitz: https://stackblitz.com/edit/stackblitz-starters-xwxwca?file=src%2Ffruit-service%2Ffruit-service.port.ts
Wrapping Up
Adopting Ports and Adapters (Hexagonal) architecture in Angular helps structure your application and offers a solid foundation for maintaining and scaling projects over time. Separating core business logic from external infrastructure details provides greater flexibility and improves testability. Angular's robust TypeScript support and Dependency Injection mechanism make this architecture straightforward to implement, letting you define clear contracts (ports) and concrete implementations (adapters) with ease. As your application grows, this pattern's advantages become increasingly evident, making your codebase more modular, adaptable, and resilient to change. For a well-organized, maintainable Angular application, Ports and Adapters is a strategic choice that can pave the way for long-term project success.
