IMT Atlantique - 22 Septembre 2025
Graduated in 2018
Core developer at Akeneo
Senior Engineer at Spendesk
Founding Engineer at Hyperline
1. S.O.L.I.D
2. Hexagonal architecture theory and live coding
- Easy to test
- Easy to evolve / maintain
- Easy to understand
- Easy to release
- Secured
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.
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
}
Violating principle
Not Violating principle
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.
Une classe doit être ouverte à l'extension, mais fermée à la modification.
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
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.
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.
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.");
}
}
}
Code Reusability: You can use subclasses anywhere the parent class is used.
Clients should not be forced to depend on interfaces they do not use
Une interface ne doit pas obliger une classe à implémenter des méthodes dont elle n'a pas besoin.
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.");
}
}
Simplicity: interfaces are small and focused
Maintenability
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.
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau, mais tous deux doivent dépendre d'abstractions.
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(', ')}`);
}
}
Flexibility: Easily extend your code with new features without changing existing code.
and accidental complexity (developer comprehension / framework / over-engineering / just in case)
complexity = accidental * mandatory * essential
complexity = accidental + mandatory + essential
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.
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.
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
switch to branch 1_functional
1. Our domain model: Boardgame + Play
2. a use case: be able to save a game with a correct number of players
Results
Switch to branch 2_controller
1. An HTTP Framework (Fastify)
2. Interact with our use case
Switch to branch 3_functional
We have an highly decoupled code
Easy to test
Focused on business logic
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)
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