Hexagonal Architecture and beyond
IMT Atlantique - 22 Septembre 2025

Hey 👋, I'm Anaël Chardan
Graduated in 2018


Core developer at Akeneo


Senior Engineer at Spendesk

Founding Engineer at Hyperline


Agenda
1. S.O.L.I.D
2. Hexagonal architecture theory and live coding
As software engineers what is our goal ?
Delivering business value while building a software
What makes a good software (as engineer)?
- Easy to test
- Easy to evolve / maintain
- Easy to understand
- Easy to release
- Secured
We want to avoid
-
Toxic code
-
Uncontrollable code
-
Uncontrolled technical debt
S.O.L.I.D
Single Responsability Principle (SRP)
Definition
The Single Responsibility Principle states that a class or a function should have only one job or responsibility. In other words, it should only do one thing and do it well.
Definition
Le principe de responsabilité unique (SRP) stipule qu'une classe ou un module ne doit avoir qu'une seule raison de changer, c'est-à-dire être responsable d'une seule fonctionnalité ou tâche.
interface BoardgameService {
playMove(row: number, column: number): void;
renderBoard(): void;
logMove(row: number, column: number): void;
}
interface BoardRenderer {
render(board: Board): void
}
interface PlayLogger {
log(play: Play): void
}
interface Play {
move(row: number, column: number): void
}
Examples
Violating principle
Not Violating principle
Pros
- Easier to read
- Easier to understand
- Easier to test
- Easier to maintain
Open/Close principle
Definition
Software components (like classes, modules, or functions) should be open for extension but closed for modification. This means you should be able to add new functionality without changing existing code.
Definition
Une classe doit être ouverte à l'extension, mais fermée à la modification.
Examples
class MoveHandler {
public handleMove(move: any): void {
if (move.type === 'placePiece') {
console.log(`Placing piece at (${move.x}, ${move.y})`);
} else if (move.type === 'drawCard') {
console.log(`Drawing a card from the ${move.deck} deck`);
} else if (move.type === 'rollDice') {
console.log(`Rolling ${move.dice} dice`);
} else {
throw new Error('Move type not supported');
}
}
}
Violating principle
interface Move {
execute(): void;
}
class PlacePieceMove implements Move {
execute(): void { /** ... */ }
}
class DrawCardMove implements Move {
execute(): void { /** ... */ }
}
class MoveHandler {
public handleMove(move: Move): void {
move.execute();
}
}
Highlighting principle
Pros
- Helps prevent breaking existing features when new features are added.
- Makes your code more flexible and easier to expand over time.
- Supports adding new behavior with minimal changes
Liskov Substitution Principle
Definition
The Liskov Substitution Principle (LSP) states that objects of a subclass should be able to replace objects of a superclass without altering the correctness of the program. In simpler terms, a subclass should behave like its parent class, so you can use it wherever the parent class is expected without causing errors or unexpected behavior.
Definition
Les objets d'une classe dérivée doivent pouvoir remplacer les objets de leur classe de base sans altérer le comportement du programme.
Example
Violating principle
Not violating principle
class BoardGame {
protected players: string[] = [];
public addPlayer(player: string): void {
this.players.push(player);
console.log(`Adding player: ${player}`);
}
}
class ChessGame extends BoardGame {
public addPlayer(player: string): void {
super.addPlayer(player); // Leverage the base class functionality
if (this.players.length > 2) {
console.log("Chess usually requires exactly 2 players, but the game can still be set up.");
}
}
}
Pros
-
Code Reusability: You can use subclasses anywhere the parent class is used.
- Flexibility: Easily extend your code with new features without changing existing code.
- Polymorphism: Helps you write more generic and scalable code
Interface segregation principle
Definition
Clients should not be forced to depend on interfaces they do not use
Definition
Une interface ne doit pas obliger une classe à implémenter des méthodes dont elle n'a pas besoin.
Example
Violating principle
Not violating principle
interface Game {
startGame(): void;
addPlayer(player: string): void;
}
interface CardGame extends Game {
drawCard(deck: string): void;
}
//...
interface BoardGame {
startGame(): void;
addPlayer(player: string): void;
drawCard(deck: string): void; // Not all games use cards
rollDice(numDice: number): void; // Not all games use dice
}
class ChessGame implements BoardGame {
//...
public drawCard(deck: string): void {
throw new Error("Chess game does not support drawing cards.");
}
}
Pros
-
Simplicity: interfaces are small and focused
-
Maintenability
- ... you get the thing 🙈
Dependency inversion principle
Definition
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
Definition
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau, mais tous deux doivent dépendre d'abstractions.
Example
Violating principle
Not violating principle
interface DiceRoller {
rollDice(numDice: number): number[];
}
// Concrete implementation of the abstraction
class StandardDiceRoller implements DiceRoller {
public rollDice(numDice: number): number[] {
const rolls = [];
for (let i = 0; i < numDice; i++) {
rolls.push(Math.floor(Math.random() * 6) + 1);
}
return rolls;
}
}
// High-level module depending on the abstraction
class GameManager {
private diceRoller: DiceRoller;
constructor(diceRoller: DiceRoller) {
this.diceRoller = diceRoller; // Dependency injection
}
public playGame(): void {
const diceResults = this.diceRoller.rollDice(2);
console.log(`Dice results: ${diceResults.join(', ')}`);
}
}
class DiceRoller {
public rollDice(numDice: number): number[] {
const rolls = [];
for (let i = 0; i < numDice; i++) {
rolls.push(Math.floor(Math.random() * 6) + 1);
}
return rolls;
}
}
// High-level module that depends directly on the low-level module
class GameManager {
private diceRoller: DiceRoller;
constructor() {
this.diceRoller = new DiceRoller(); // Direct dependency
}
public playGame(): void {
const diceResults = this.diceRoller.rollDice(2);
console.log(`Dice results: ${diceResults.join(', ')}`);
}
}
Pros
-
Flexibility: Easily extend your code with new features without changing existing code.
- It helps the testing with easier injection
3 Tiers architecture




Fragile
Coupling = Fragile
Hexagonal Architecture
Low coupling / High Cohesion
Focus on the domain
And manage the risks of I/O
What is an I/O ?
Everything that you cannot control
- Time
- Network
- Disk
- Cache
- Database
- API
- Randomness








Control the complexity

and accidental complexity (developer comprehension / framework / over-engineering / just in case)
Without decoupling
With decoupling
complexity = accidental * mandatory * essential
complexity = accidental + mandatory + essential

What's a port ?
Driver Ports offer the application functionality to drivers of the outside world. Thus, driver ports are said to be the use case boundary of the application. They are the API of the application.
A driven port is an interface for a functionality, needed by the application for implementing the business logic. Such functionality is provided by a driven actor. So driven ports are the SPI (Service Provider Interface) required by the application. A driven port would be like a Required Interface.
What's an adapter?
A driver adapter uses a driver port interface, converting a specific technology request into a technology agnostic request to a driver port.
A driven adapter implements a driven port interface, converting the technology agnostic methods of the port into specific technology methods.
Title Text
Subtitle

Let's code !
Our use case
I want to be able to save my boardgames plays with a specific number of players.
I want to validate the number of players thanks to BoardGameGeek API
Start with the domain !
switch to branch 1_functional
What do we want?
1. Our domain model: Boardgame + Play
2. a use case: be able to save a game with a correct number of players

Results
Let's interact with it!
Switch to branch 2_controller
What do we want?
1. An HTTP Framework (Fastify)
2. Interact with our use case

Results
Let's add the interaction with BGG
Switch to branch 3_functional
Results

As a result
We have an highly decoupled code
Easy to test
Focused on business logic
What's next ?
Other decoupling possibles:
- Separate Command and Queries Concerns
- Addition of service buses to communicate to use cases (e.g Missive.js)
- Dependency injection frameworks (e.g Awilix)
Ressources
Links
-
Concepts: CQS / CQRS / Hexagonal (port and adapters) / DDD / Solid
-
https://jmgarridopaz.github.io/content/hexagonalarchitecture.html
-
Speakers: Arnaud Lemaire / Thomas Pierrain / Matthias Verraes / Arnaud Langlade / Valentina Jemuovíc / Nick Tune / Julien Topcu / Julien Janvier
-
Books: PPPDDD / Hexagonal Architecture (Alistair Cockburn
Hexagonal Architecture and beyond
By Anaël Chardan
Hexagonal Architecture and beyond
- 59