// Serverless / FaaS — Référence complète

Architecture
Serverless & FaaS

Function as a Service, cycle de vie des fonctions, patterns event-driven et exemples TypeScript (POO + Fonctionnel) — selon les principes de Robert C. Martin.

☁️

Serverless ≠ sans serveurs

Il y a toujours des serveurs. Mais c'est le fournisseur cloud qui les provisionne, les scale, et les détruit. Vous apportez votre code — l'infrastructure se matérialise à la demande, puis disparaît. C'est l'aboutissement logique d'une tendance longue : abstraire progressivement l'infrastructure jusqu'à la faire disparaître du champ de vision du développeur.

01
La métaphore de l'hôtel

Pour comprendre le serverless, imaginez trois modes d'hébergement. Chacun représente un compromis différent entre contrôle, responsabilité et coût.

ModeAnalogieÉquivalent techniqueModèle
On-PremiseVous achetez une maisonVous gérez vos serveurs physiquesContrôle total
IaaS / VMVous louez un appartementVM cloud : EC2, GCP Compute EngineBail à durée fixe
Serverless / FaaSChambre d'hôtel à la nuitAWS Lambda, Cloudflare Workers, Vercel FnPay-per-use

Le serverless pousse l'abstraction au maximum. Vous ne gérez ni le serveur, ni le runtime, ni le système d'exploitation. Vous écrivez une fonction, vous la déployez, et le cloud s'occupe du reste.

Uncle Bob — SRP — Le Single Responsibility Principle s'applique aussi à l'infrastructure. Si votre équipe gère à la fois le code métier et les serveurs, elle a deux raisons de changer. Le serverless supprime la deuxième.
02
Vue d'ensemble

Un système serverless se décompose en quatre zones. Les événements traversent ces zones de gauche à droite : de la source au service managé, en passant par le gateway et la fonction.

Clients / Sources
Web Browser
Mobile App
IoT Device
Cron / Scheduler
Message Queue
Storage Event
Gateway / Routing
API Gateway
Auth / JWT
Rate Limiter
Event Router
Load Balancer
Functions (FaaS)
fn: createOrder
fn: getUser
fn: processPayment
fn: sendEmail
fn: resizeImage
fn: dailyReport
Services Managés
DynamoDB / Firestore
S3 / Blob Storage
Redis / ElastiCache
SQS / Pub-Sub
SES / SendGrid
Cognito / Auth0

Flux de dépendance

Événement
HTTP / Queue / Cron
trigger
Gateway
Auth + Route
invoke
Function
Stateless · Éphémère
appelle
Services
DB · Cache · Queue
répond
Réponse
JSON / Event
03
Les principes fondamentaux

Six principes structurent l'architecture serverless. Chacun impose des contraintes qui, bien appliquées, produisent un design plus propre et plus résilient.

📏
Scale to Zero

Zéro instance = zéro coût

Quand personne n'appelle votre fonction, il n'y a aucune instance active. Le coût est strictement nul. C'est l'inverse d'un serveur traditionnel qui tourne 24h/24 même sans charge.

Auto-scaling0 → ∞
Pay-per-use

Facturation à la milliseconde

Vous payez uniquement le temps d'exécution réel. AWS Lambda facture par incréments de 1ms. 10 000 invocations d'une fonction qui prend 100ms coûtent quelques centimes.

Granularité msCoût variable
« Pas d'appel = pas de facture. »
🔮
Stateless

Aucun état entre invocations

Chaque invocation repart de zéro. Aucun état local ne persiste entre deux appels. L'état doit être déporté vers des services externes : DynamoDB, Redis, S3. C'est une contrainte qui force un bon design.

ImmutablePure functions
🎯
Event-Driven

Déclenchement par événement

Les fonctions ne tournent pas en permanence — elles réagissent. Un événement HTTP, un message dans une queue SQS, un upload S3, un timer Cron : chaque source peut déclencher une fonction.

HTTPSQSS3 EventsCron
📦
FaaS Unit

La fonction comme unité de déploiement

Contrairement aux microservices (qui déploient un service), FaaS déploie une fonction individuelle. Chaque fonction peut avoir son propre runtime, sa mémoire, son timeout, ses permissions IAM.

Fine-grainedIndépendant
🔒
Managed Infrastructure

Zero ops pour le développeur

Pas de serveur à patcher, pas de load balancer à configurer, pas de certificat SSL à renouveler. Le fournisseur cloud gère la haute disponibilité, la sécurité OS, les mises à jour runtime.

No-opsHA automatique
04
Cold Start & Warm

Le cold start est le talon d'Achille du serverless. Quand aucun container n'est disponible, le cloud doit en créer un de zéro. Ce processus prend entre 200ms et 2 secondes selon le runtime et la taille du package.

Cycle de vie d'une invocation

Cold — étape 1
Invocation
Un événement arrive. Aucun container disponible.
Cold — étape 2
Init Container
Le cloud provider alloue les ressources (CPU, RAM, réseau).
Cold — étape 3
Download Code
Le package ZIP ou l'image container est téléchargé.
Cold — étape 4
Boot Runtime
Node.js / Python / JVM démarre. Les plus lents : JVM, .NET.
Init module
Module Init
Le code hors handler s'exécute (imports, clients DB, DI…).
Execution
Execute
Le handler s'exécute. Le container reste chaud ≈ 5-15 min.

Comparaison

❄️ Cold Start — 200ms à 2s
Première invocation après déploiement
Après ≈ 15 min d'inactivité (container détruit)
Pic de trafic : scale-out → nouveaux containers
JVM / .NET particulièrement pénalisés
Risque UX sur API synchrones critiques
🔥 Warm — 1ms à 5ms
Container réutilisé, runtime déjà démarré
Variables module-level persistées (clients DB, cache)
Provisioned Concurrency : containers pre-warmed
Node.js / Python : cold starts très courts (<100ms)
Trafic régulier → containers majoritairement chauds
Règle : Initialisez les clients DB, SDK AWS et caches au niveau module (hors du handler). Ils seront créés une seule fois au cold start, puis réutilisés gratuitement à chaque invocation warm.
05
Clean Architecture dans une Lambda

Le handler Lambda est un Controller au sens d'Uncle Bob : il traduit l'entrée HTTP (ou SQS, S3…) en appel au Use Case, puis traduit le résultat en réponse HTTP. Aucune logique métier dans le handler.

Structure des fichiers
src/

├── domain/ ← Cercle 1 + 2 : entités + use cases
│ ├── entities/
│ │ └── Order.ts ← Entité pure, zéro import AWS
│ ├── usecases/
│ │ └── CreateOrderUseCase.ts ← Use case + ports (interfaces)
│ └── ports/
│ ├── OrderRepository.ts ← Interface (jamais implémentée ici)
│ └── EventPublisher.ts

├── infrastructure/ ← Cercle 3 : adaptateurs concrets
│ ├── DynamoOrderRepository.ts ← Implémente OrderRepository
│ └── SnsEventPublisher.ts ← Implémente EventPublisher

└── handlers/ ← Cercle 4 : handler Lambda (adaptateur AWS)
└── createOrder.ts ← Composition Root + traduction HTTP↔domaine
domain/entities/Order.ts
// Entité — zéro dépendance externe, zéro import AWS / TypeORM class Order { constructor( public readonly id: string, public readonly userId: string, public readonly items: OrderItem[], public readonly status: OrderStatus = 'pending' ) {} get total(): number { return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0); } confirm(): Order { if (this.items.length === 0) { throw new Error('Cannot confirm empty order'); } // Retourne une nouvelle instance — immutabilité return new Order(this.id, this.userId, this.items, 'confirmed'); } } type OrderStatus = 'pending' | 'confirmed' | 'cancelled'; interface OrderItem { productId: string; quantity: number; price: number; }
domain/ports/OrderRepository.ts
// Port — interface définie par le domaine, implémentée par l'infrastructure interface OrderRepository { save(order: Order): Promise<void>; findById(id: string): Promise<Order | null>; } interface EventPublisher { publish(event: OrderCreatedEvent): Promise<void>; } interface OrderCreatedEvent { type: 'ORDER_CREATED'; orderId: string; userId: string; total: number; timestamp: string; }
domain/usecases/CreateOrderUseCase.ts
// Use Case — orchestre domaine et ports, sans connaître AWS class CreateOrderUseCase { constructor( private readonly repository: OrderRepository, // injection de dépendance private readonly publisher: EventPublisher ) {} async execute(input: CreateOrderInput): Promise<CreateOrderOutput> { // 1. Créer et valider via l'entité const order = new Order(crypto.randomUUID(), input.userId, input.items); const confirmed = order.confirm(); // 2. Persister via le port (jamais via DynamoDB directement) await this.repository.save(confirmed); // 3. Publier l'événement domaine await this.publisher.publish({ type: 'ORDER_CREATED', orderId: confirmed.id, userId: confirmed.userId, total: confirmed.total, timestamp: new Date().toISOString() }); return { orderId: confirmed.id, total: confirmed.total }; } }
infrastructure/DynamoOrderRepository.ts
import { DynamoDB } from '@aws-sdk/client-dynamodb'; class DynamoOrderRepository implements OrderRepository { private client = new DynamoDB({}); // init module-level → warm reuse async save(order: Order): Promise<void> { await this.client.putItem({ TableName: process.env.ORDERS_TABLE!, Item: { PK: { S: `ORDER#${order.id}` }, userId: { S: order.userId }, status: { S: order.status }, total: { N: String(order.total) }, items: { S: JSON.stringify(order.items) } } }); } async findById(id: string): Promise<Order | null> { const result = await this.client.getItem({ TableName: process.env.ORDERS_TABLE!, Key: { PK: { S: `ORDER#${id}` } } }); if (!result.Item) return null; return new Order( id, result.Item.userId.S!, JSON.parse(result.Item.items.S!), result.Item.status.S as OrderStatus ); } }
handlers/createOrder.ts
import { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda'; // ─── Composition Root ─────────────────────────────────────────────────────────── // Instancié au niveau MODULE : exécuté une seule fois au cold start, // puis réutilisé gratuitement par toutes les invocations warm. const useCase = new CreateOrderUseCase( new DynamoOrderRepository(), new SnsEventPublisher() ); // ─── Handler ────────────────────────────────────────────────────────────────── // Rôle : traduire APIGatewayProxyEvent → domaine → APIGatewayProxyResult // Aucune logique métier ici — pure traduction (Uncle Bob : Controller) export const handler: APIGatewayProxyHandler = async (event): Promise<APIGatewayProxyResult> => { try { const body = JSON.parse(event.body ?? '{}'); const result = await useCase.execute({ userId: event.requestContext.authorizer?.userId ?? body.userId, items: body.items }); return { statusCode: 201, body: JSON.stringify(result), headers: { 'Content-Type': 'application/json' } }; } catch (error) { const message = error instanceof Error ? error.message : 'Internal error'; return { statusCode: 400, body: JSON.stringify({ error: message }) }; } };
06
Approche fonctionnelle — Railway Oriented

L'approche Railway Oriented Programming modélise chaque étape comme une fonction pure qui retourne Ok ou Err. La première erreur court-circuite le pipeline — comme un train qui déraille sur une voie parallèle.

lib/result.ts
// Type algébrique — représente succès OU erreur, jamais les deux type Result<T, E = Error> = | { readonly _tag: 'Ok'; readonly value: T } | { readonly _tag: 'Err'; readonly error: E }; // Constructeurs const Ok = <T>(value: T): Result<T> => ({ _tag: 'Ok', value }); const Err = <E>(error: E): Result<never, E> => ({ _tag: 'Err', error }); // Combinateurs fonctionnels const map = <T, U, E>(result: Result<T, E>, fn: (v: T) => U): Result<U, E> => result._tag === 'Ok' ? Ok(fn(result.value)) : result; const flatMap = <T, U, E>(result: Result<T, E>, fn: (v: T) => Result<U, E>): Result<U, E> => result._tag === 'Ok' ? fn(result.value) : result;
domain/order.ts
// Types discriminés — le type porte l'état type PendingOrder = { _tag: 'Pending'; id: string; userId: string; items: OrderItem[] }; type ConfirmedOrder = { _tag: 'Confirmed'; id: string; userId: string; items: OrderItem[]; total: number }; // Fonctions pures — aucun effet de bord const createOrder = (userId: string, items: OrderItem[]): PendingOrder => ({ _tag: 'Pending', id: crypto.randomUUID(), userId, items }); const validateOrder = (order: PendingOrder): Result<PendingOrder> => order.items.length > 0 ? Ok(order) : Err(new Error('Order must have at least one item')); const calculateTotal = (items: OrderItem[]): number => items.reduce((sum, item) => sum + item.price * item.quantity, 0); const confirmOrder = (order: PendingOrder): ConfirmedOrder => ({ _tag: 'Confirmed', ...order, total: calculateTotal(order.items) });
handler.ts
// Types des dépendances injectées par paramètre (pas de classe) type SaveOrder = (order: ConfirmedOrder) => Promise<Result<void>>; type PublishEvent = (event: OrderEvent) => Promise<Result<void>>; // Factory de pipeline — retourne une fonction avec les dépendances capturées const createOrderPipeline = ( saveOrder: SaveOrder, publishEvent: PublishEvent ) => async (input: { userId: string; items: OrderItem[] }): Promise<Result<{ orderId: string; total: number }>> => { // ── Railway ──────────────────────────────────────────────────────────────────── // Chaque étape court-circuite sur Err — comme && mais pour les Promises const pending = createOrder(input.userId, input.items); const validated = validateOrder(pending); if (validated._tag === 'Err') return validated; const confirmed = confirmOrder(validated.value); const saved = await saveOrder(confirmed); if (saved._tag === 'Err') return saved; const published = await publishEvent({ type: 'ORDER_CREATED', orderId: confirmed.id, total: confirmed.total }); if (published._tag === 'Err') return published; return Ok({ orderId: confirmed.id, total: confirmed.total }); }; // ── Handler fonctionnel ───────────────────────────────────────────────────────── const pipeline = createOrderPipeline( makeDynamoSaveOrder(), // adaptateur instancié au niveau module makeSnsPublishEvent() ); export const handler: APIGatewayProxyHandler = async (event) => { const body = JSON.parse(event.body ?? '{}'); const result = await pipeline({ userId: body.userId, items: body.items }); if (result._tag === 'Err') return { statusCode: 400, body: JSON.stringify({ error: result.error.message }) }; return { statusCode: 201, body: JSON.stringify(result.value) }; };
Zéro classe, zéro this. — L'approche fonctionnelle élimine les classes au profit de fonctions pures et de types discriminés. Les dépendances sont injectées par paramètre, pas par constructeur. Le résultat est plus facile à tester et à raisonner.
07
Patterns Serverless avancés

Au-delà du simple handler, des patterns architecturaux spécifiques au serverless permettent de gérer la coordination, la résilience et le parallélisme à grande échelle.

💃
Event Choreography

Chorégraphie sans chef d'orchestre

Chaque fonction réagit aux événements SNS/SQS de manière autonome. Pas de coordinateur central. Plus résilient : la panne d'une fonction n'interrompt pas les autres. Idéal pour les workflows asynchrones.

SNSSQSEventBridge
« createOrder → publie ORDER_CREATED → processPayment réagit »
🎻
Event Orchestration

Chef d'orchestre central

AWS Step Functions ou Temporal coordonnent l'exécution des fonctions en séquence ou en parallèle. Visibilité totale sur l'état du workflow. Idéal pour les processus métier critiques avec compensation (saga pattern).

Step FunctionsSagaRetry
🛡️
Idempotency

Protection contre les doublons

Les queues garantissent "at-least-once delivery" : une fonction peut être invoquée plusieurs fois pour le même événement. L'idempotency key, stockée en DynamoDB, permet de dédupliquer les traitements.

At-least-onceDedupDynamoDB TTL
🌊
Fan-out / Fan-in

Parallélisme massif

Une fonction publie N événements (fan-out), N fonctions traitent en parallèle, une dernière agrège les résultats (fan-in). Exemple : générer 1000 rapports PDF simultanément en quelques secondes.

ParallélismeSNS Fanout
lib/idempotency.ts
// Les fonctions peuvent être invoquées plusieurs fois (at-least-once delivery) // L'idempotency key évite les doublons const processWithIdempotency = async <T>( idempotencyKey: string, handler: () => Promise<T> ): Promise<T | undefined> => { // Vérification en DynamoDB (conditional write — atomique) try { await dynamoDB.putItem({ TableName: 'idempotency-store', Item: { PK: { S: idempotencyKey }, ttl: { N: String(Date.now() / 1000 + 86400) } }, ConditionExpression: 'attribute_not_exists(PK)' // échoue si déjà traité }); } catch (e) { console.log(`Skipping — already processed: ${idempotencyKey}`); return undefined; } return handler(); }; // Utilisation dans un handler SQS export const processPaymentHandler = async (event: SQSEvent) => { for (const record of event.Records) { const { orderId, amount } = JSON.parse(record.body); await processWithIdempotency(`payment-${orderId}`, async () => { // logique de paiement — ne s'exécute qu'une seule fois }); } };
choreography.ts
// fn: createOrder — publie ORDER_CREATED sur SNS export const createOrderHandler: APIGatewayProxyHandler = async (event) => { // ... traitement commande ... await sns.publish({ TopicArn: process.env.ORDERS_TOPIC_ARN!, Message: JSON.stringify({ type: 'ORDER_CREATED', orderId, userId, amount }), MessageAttributes: { eventType: { DataType: 'String', StringValue: 'ORDER_CREATED' } } }); return { statusCode: 201, body: JSON.stringify({ orderId }) }; }; // fn: processPayment — souscrit à ORDER_CREATED (filtre SNS → SQS → Lambda) export const processPaymentHandler = async (event: SQSEvent) => { for (const record of event.Records) { const orderEvent = JSON.parse(record.body); // Traiter le paiement, puis publier PAYMENT_PROCESSED... } }; // fn: sendConfirmationEmail — souscrit à PAYMENT_PROCESSED export const sendEmailHandler = async (event: SQSEvent) => { // Chaque fonction est totalement autonome — découplage maximal };
08
Forces, limites et décision

Le serverless n'est pas une solution universelle. Voici un tableau d'aide à la décision pour évaluer si cette architecture convient à votre contexte.

AspectDétailVerdict
ScalabilitéDe 0 à 10 000 instances en quelques secondes. Automatique, sans configuration.Force majeure
Coût (trafic faible)Pay-per-use + scale to zero. MVP, trafic irrégulier : excellent ROI.Idéal
Coût (charge constante)À forte charge continue, des containers/VMs longue durée deviennent plus économiques.À évaluer
Cold Start200ms–2s de latence à la première invocation. Atténuable avec Provisioned Concurrency.Attention API sync
Durée max d'exécutionAWS Lambda : 15 minutes maximum. Inadapté aux traitements longs.Limite hard
Debugging distribuéLogs éparpillés sur N fonctions. Nécessite X-Ray, OpenTelemetry, ou Datadog.Outillage requis
Complexité opérationnelleZéro serveur à gérer. Idéal pour petites équipes sans ops dédiés.Avantage clair
Design du codeForce le stateless et le SRP. Naturellement aligné avec Clean Architecture.Bonne pratique
Vendor lock-inDépendance forte aux APIs du fournisseur (Lambda, DynamoDB, SNS…).Risque
Tests unitairesDomaine pur très facile à tester. Intégration avec le cloud plus complexe (LocalStack).Domaine : trivial

Quand l'utiliser ?

Serverless est idéal quand…
Trafic irrégulier / imprévisible — pics et creux
APIs légères, webhooks, traitements événementiels
MVP / prototypes — time-to-market rapide
Traitements batch asynchrones (resize, exports, rapports)
Équipes petites sans ops dédiés
Microservices avec domaines bien délimités (DDD)
À éviter quand…
Charge continue et élevée — containers plus économiques
Latence < 50ms requise en permanence — cold start risqué
Traitements longs (> 15 minutes)
État local nécessaire — sessions, WebSockets persistants
Forte réticence au vendor lock-in
Logique monolithique couplée impossible à découper
🧭

La règle de dépendance s'applique toujours

Peu importe l'infrastructure — Lambda, containers, serveurs — la règle de dépendance d'Uncle Bob reste invariante : les dépendances pointent vers l'intérieur. Le domaine ne connaît pas Lambda. La Clean Architecture est agnostique au mode de déploiement. C'est précisément ce qui vous permet de migrer d'une Lambda vers un container Docker sans changer une ligne de logique métier.

Domaine / Entités
Use Cases / Ports
Adaptateurs / Infrastructure
Handler Lambda (frontière)
FaaS / Functions
Cold Start / Violation