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.
01La 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.
Mode
Analogie
Équivalent technique
Modèle
On-Premise
Vous achetez une maison
Vous gérez vos serveurs physiques
Contrôle total
IaaS / VM
Vous louez un appartement
VM cloud : EC2, GCP Compute Engine
Bail à durée fixe
Serverless / FaaS
Chambre d'hôtel à la nuit
AWS Lambda, Cloudflare Workers, Vercel Fn
Pay-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.
02Vue 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
03Les 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
04Cold 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.
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.
05Clean 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.
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 = newCreateOrderUseCase(
newDynamoOrderRepository(),
newSnsEventPublisher()
);
// ─── Handler ──────────────────────────────────────────────────────────────────
// Rôle : traduire APIGatewayProxyEvent → domaine → APIGatewayProxyResult
// Aucune logique métier ici — pure traduction (Uncle Bob : Controller)export consthandler: 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 instanceofError ? error.message : 'Internal error';
return { statusCode: 400, body: JSON.stringify({ error: message }) };
}
};
06Approche 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 deuxtypeResult<T, E = Error> =
| { readonly _tag: 'Ok'; readonly value: T }
| { readonly _tag: 'Err'; readonly error: E };
// ConstructeursconstOk = <T>(value: T): Result<T> => ({ _tag: 'Ok', value });
constErr = <E>(error: E): Result<never, E> => ({ _tag: 'Err', error });
// Combinateurs fonctionnelsconstmap = <T, U, E>(result: Result<T, E>, fn: (v: T) => U): Result<U, E> =>
result._tag === 'Ok' ? Ok(fn(result.value)) : result;
constflatMap = <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'étattypePendingOrder = { _tag: 'Pending'; id: string; userId: string; items: OrderItem[] };
typeConfirmedOrder = { _tag: 'Confirmed'; id: string; userId: string; items: OrderItem[]; total: number };
// Fonctions pures — aucun effet de bordconstcreateOrder = (userId: string, items: OrderItem[]): PendingOrder => ({
_tag: 'Pending', id: crypto.randomUUID(), userId, items
});
constvalidateOrder = (order: PendingOrder): Result<PendingOrder> =>
order.items.length > 0
? Ok(order)
: Err(newError('Order must have at least one item'));
constcalculateTotal = (items: OrderItem[]): number =>
items.reduce((sum, item) => sum + item.price * item.quantity, 0);
constconfirmOrder = (order: PendingOrder): ConfirmedOrder => ({
_tag: 'Confirmed', ...order, total: calculateTotal(order.items)
});
handler.ts
// Types des dépendances injectées par paramètre (pas de classe)typeSaveOrder = (order: ConfirmedOrder) => Promise<Result<void>>;
typePublishEvent = (event: OrderEvent) => Promise<Result<void>>;
// Factory de pipeline — retourne une fonction avec les dépendances capturéesconstcreateOrderPipeline = (
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 Promisesconst pending = createOrder(input.userId, input.items);
const validated = validateOrder(pending);
if (validated._tag === 'Err') return validated;
const confirmed = confirmOrder(validated.value);
const saved = awaitsaveOrder(confirmed);
if (saved._tag === 'Err') return saved;
const published = awaitpublishEvent({
type: 'ORDER_CREATED', orderId: confirmed.id, total: confirmed.total
});
if (published._tag === 'Err') return published;
returnOk({ orderId: confirmed.id, total: confirmed.total });
};
// ── Handler fonctionnel ─────────────────────────────────────────────────────────const pipeline = createOrderPipeline(
makeDynamoSaveOrder(), // adaptateur instancié au niveau modulemakeSnsPublishEvent()
);
export consthandler: APIGatewayProxyHandler = async (event) => {
const body = JSON.parse(event.body ?? '{}');
const result = awaitpipeline({ 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.
07Patterns 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.
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 doublonsconstprocessWithIdempotency = 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;
}
returnhandler();
};
// Utilisation dans un handler SQSexport constprocessPaymentHandler = async (event: SQSEvent) => {
for (const record of event.Records) {
const { orderId, amount } = JSON.parse(record.body);
awaitprocessWithIdempotency(`payment-${orderId}`, async () => {
// logique de paiement — ne s'exécute qu'une seule fois
});
}
};
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.