Designing Data-Intensive Applications
Chapitre 11 / 11 · 19 min de lecture

Le traitement de flux (streaming)

Traiter les données en continu : systèmes de messagerie, logs partitionnés (Kafka), CDC, event sourcing et raisonnement sur le temps.

Le traitement par lots (batch processing) du chapitre précédent reposait sur une hypothèse silencieuse mais décisive : l'entrée est bornée (bounded), c'est-à-dire de taille connue et finie. Un tri à la MapReduce doit lire la totalité de son entrée avant de produire le moindre résultat, car le tout dernier enregistrement pourrait porter la clé la plus petite et devoir sortir en premier. Or, dans la réalité, beaucoup de données sont non bornées (unbounded) : vos utilisateurs ont produit des données hier, en produisent aujourd'hui et en produiront demain. Le flux ne se termine jamais, et les données ne sont donc jamais « complètes » au sens strict.

Pour contourner ce problème, les systèmes batch découpent artificiellement le temps en tranches fixes : on traite une journée de données à la fin de chaque journée. Mais ce délai d'un jour est insupportable pour des usagers impatients. On peut alors traiter une seconde de données à la fin de chaque seconde, ou — en abandonnant tout découpage temporel — traiter chaque événement au fil de l'eau, dès qu'il survient. C'est l'idée du traitement de flux (stream processing) : la contrepartie non bornée et incrémentale du batch, qui réduit drastiquement la latence. Ce chapitre suit le parcours d'un flux : comment il est transmis, comment il se relie aux bases de données, puis comment on le traite réellement.

Événements et flux : l'unité de base

Lorsque l'entrée est un fichier, la première étape consiste à l'analyser en une suite d'enregistrements. En contexte streaming, un enregistrement se nomme un événement (event), mais c'est la même chose : un objet petit, autonome et immuable décrivant quelque chose qui s'est produit à un instant donné. Un événement contient en général un horodatage (timestamp). Cela peut être une action humaine (consulter une page, faire un achat) ou une mesure machine (la température d'un capteur, l'usage CPU).

Un événement est généré une seule fois par un producteur (producer, aussi appelé publisher ou sender), puis potentiellement traité par plusieurs consommateurs (consumers, subscribers). Comme un nom de fichier regroupe des enregistrements liés, un système de flux regroupe les événements apparentés dans un sujet (topic) ou flux. L'événement peut être encodé en texte, en JSON ou sous forme binaire, ce qui permet de le stocker (en l'ajoutant à un fichier, une table relationnelle, un document) ou de l'envoyer sur le réseau.

{
  "event_type": "page_view",
  "user_id": "u-9381",
  "url": "/products/42",
  "session_id": "s-7c2a",
  "timestamp": "2026-06-03T14:22:31.412Z"
}

En principe, un fichier ou une base suffirait : le producteur écrit, les consommateurs interrogent (poll) périodiquement. Mais le sondage devient coûteux dès qu'on vise un faible délai : plus on sonde souvent, plus la proportion de requêtes qui ne ramènent rien augmente, et plus la surcharge croît. Il vaut mieux notifier les consommateurs quand un événement apparaît. Les bases relationnelles offrent bien des déclencheurs (triggers), mais ils sont limités et furent toujours une arrière-pensée de conception. D'où l'essor d'outils spécialisés.

Transmettre les flux : les systèmes de messagerie

Un système de messagerie (messaging system) pousse les événements vers les consommateurs au lieu de les faire sonder. Un simple tuyau Unix ou une connexion TCP relient un émetteur à un destinataire ; un système de messagerie, lui, autorise plusieurs producteurs vers un même sujet et plusieurs consommateurs sur ce sujet (modèle publication-abonnement, ou publish-subscribe). Pour différencier ces systèmes, deux questions sont éclairantes.

Que se passe-t-il si les producteurs vont plus vite que les consommateurs ? Trois réponses : jeter des messages, les mettre en file d'attente (buffer), ou exercer une contre-pression (backpressure) qui bloque le producteur. Les tuyaux Unix et TCP utilisent la contre-pression : un petit tampon de taille fixe, et l'émetteur se bloque s'il déborde.

Que se passe-t-il si un nœud tombe ? Des messages sont-ils perdus ? Comme pour une base, la durabilité exige une combinaison d'écriture disque et de réplication, ce qui a un coût. Si l'application tolère des pertes occasionnelles (par exemple des relevés de capteur réémis peu après), on gagne en débit et en latence. Si l'on compte des événements, en revanche, chaque message perdu fausse un compteur.

Messagerie directe et brokers

Certains systèmes relient producteurs et consommateurs directement, sans intermédiaire : multidiffusion UDP pour les flux boursiers (faible latence), bibliothèques sans broker comme ZeroMQ, collecte de métriques par UDP non fiable (StatsD), ou requêtes HTTP/RPC vers un consommateur exposant un service — c'est l'idée des webhooks. Ces systèmes fonctionnent dans leur niche, mais exigent que le code applicatif soit conscient du risque de perte : ils supposent producteurs et consommateurs constamment en ligne. Un consommateur hors-ligne rate les messages émis pendant son absence.

L'alternative répandue est le broker de messages (message broker, ou file d'attente de messages), une sorte de base de données optimisée pour les flux de messages. Il tourne comme un serveur ; producteurs et consommateurs s'y connectent en clients. En centralisant les données dans le broker, on tolère facilement des clients qui vont et viennent, et la question de la durabilité se déplace vers le broker. Les consommateurs sont alors généralement asynchrones : le producteur attend juste la confirmation que le broker a bufferisé le message, pas qu'il soit traité.

Note

Un broker ressemble à une base, mais en diffère sur des points pratiques majeurs : une base conserve les données jusqu'à suppression explicite, alors que la plupart des brokers effacent un message dès qu'il a été livré avec succès. Ils supposent donc un jeu de travail (working set) restreint — des files courtes. Et là où une base répond par un instantané ponctuel, un broker notifie les clients à l'arrivée de nouvelles données.

Plusieurs consommateurs, accusés de réception

Quand plusieurs consommateurs lisent un même sujet, deux schémas coexistent. En répartition de charge (load balancing), chaque message va à l'un des consommateurs : on partage le travail pour paralléliser. En diffusion (fan-out), chaque message va à tous les consommateurs : plusieurs traitements indépendants « écoutent » la même émission. Les deux se combinent (plusieurs groupes, chacun recevant tout, mais un seul nœud par groupe traitant chaque message).

Pour ne pas perdre un message en cas de panne d'un consommateur, le broker utilise des accusés de réception (acknowledgements) : le client signale explicitement qu'il a fini de traiter un message, et le broker l'efface. Sans accusé reçu à temps, le broker redélivre le message à un autre consommateur. Conséquence subtile, combinée à la répartition de charge : si le consommateur 2 plante en traitant m3 pendant que le consommateur 1 traite m4, alors m3 est redélivré à 1, qui traite donc m4, m3, m5 — l'ordre n'est plus respecté. Sans conséquence si les messages sont indépendants, mais problématique en cas de dépendances causales.

Le log partitionné : durabilité et relecture

Les brokers traditionnels gardent une mentalité transitoire : effacer après livraison. Bases et systèmes de fichiers font l'inverse : tout écrit est conservé jusqu'à suppression explicite. Cette différence a un impact énorme. Avec un broker AMQP/JMS, recevoir un message est destructeur : on ne peut pas relancer le même consommateur et obtenir le même résultat. Un nouveau consommateur ne reçoit que les messages postérieurs à son inscription. Pourquoi ne pas combiner la durabilité d'une base avec la notification basse latence de la messagerie ? C'est l'idée du broker à base de log (log-based message broker).

Un log est une simple séquence d'enregistrements en ajout-seul (append-only) sur disque. Le producteur ajoute à la fin ; le consommateur lit séquentiellement et attend une notification s'il atteint la fin — exactement comme tail -f. Pour dépasser le débit d'un seul disque, le log est partitionné (partitioned) : différentes partitions sur différentes machines, plusieurs partitions formant un sujet. Dans chaque partition, le broker attribue à chaque message un numéro de séquence croissant, le décalage (offset). Les messages sont totalement ordonnés au sein d'une partition, mais sans garantie d'ordre entre partitions. Apache Kafka, Amazon Kinesis et DistributedLog fonctionnent ainsi, atteignant des millions de messages par seconde.

// Consommation d'un log partitionné par offset.
// Le consommateur garde la trace de sa position ; le broker
// n'a pas à suivre chaque message individuellement.

type Offset = number;

interface Message {
  offset: Offset;
  key: string;
  value: unknown;
}

async function consume(
  partition: AsyncIterable<Message>,
  fromOffset: Offset,
  handle: (msg: Message) => Promise<void>,
): Promise<void> {
  let committed = fromOffset;
  for await (const msg of partition) {
    if (msg.offset < committed) continue; // déjà traité
    await handle(msg);
    committed = msg.offset; // on n'avance que l'offset
  }
}

Le décalage joue le rôle du numéro de séquence d'un log de réplication : le broker se comporte comme un leader, le consommateur comme un suiveur (follower). Si un consommateur tombe, un autre nœud du groupe reprend sa partition au dernier offset enregistré. Comme le log croît indéfiniment, il est découpé en segments ; les vieux segments sont supprimés ou archivés. Le log se comporte donc comme un tampon circulaire (ring buffer) de taille bornée mais vaste : un disque de 6 To à 150 Mo/s met environ onze heures à se remplir, soit en pratique plusieurs jours ou semaines de messages. On peut surveiller le retard d'un consommateur et alerter avant qu'il ne perde des messages — et un consommateur lent ne gêne que lui-même.

Astuce

L'atout décisif du log : consommer est non destructeur, comme lire un fichier. On peut démarrer une copie d'un consommateur avec les offsets d'hier, écrire ailleurs, et rejouer (replay) la dernière journée autant de fois qu'on veut, en variant le code. Cela rapproche le log-based du batch : la donnée dérivée est séparée de l'entrée par une transformation répétable, propice à l'expérimentation et à la récupération après bug.

Le tableau ci-dessous résume le choix entre les deux familles.

CritèreFile AMQP/JMSLog partitionné (Kafka)
AffectationUn message à un consommateurUne partition entière à un nœud
Après traitementMessage effacé (destructeur)Offset avancé, log conservé
Relecture des anciens messagesImpossibleTriviale (rejouer depuis un offset)
Ordre des messagesCassé par répartition + redélivranceGaranti dans une partition
ParallélismeMessage par messageBorné par le nombre de partitions
Idéal pourTâches coûteuses, ordre indifférentFort débit, traitement rapide, ordre important

Bases de données et flux

Le lien entre bases et flux est fondamental, pas seulement physique. Un log de réplication est un flux d'événements d'écriture, produit par le leader. Le principe de la réplication par machine à états (state machine replication) le confirme : si chaque événement est une écriture et si chaque réplique traite les mêmes événements dans le même ordre (de façon déterministe), toutes finissent dans le même état. Ce n'est qu'un cas de flux d'événements.

Le danger des écritures doubles

Aucun système unique ne couvre tous les besoins : une base OLTP pour les requêtes, un cache, un index plein texte, un entrepôt analytique — chacun avec sa propre copie des données. Il faut les garder synchronisés (keeping systems in sync). Avec un entrepôt, on le fait par ETL (un batch). Si trop lent, on tente parfois des écritures doubles (dual writes) : le code écrit explicitement dans chaque système. C'est dangereux.

Deux clients veulent mettre X à A et à B. À cause d'un entrelacement malheureux, la base voit A puis B (final : B), tandis que l'index voit B puis A (final : A). Les deux systèmes sont définitivement incohérents, sans qu'aucune erreur n'ait été signalée. Autre problème : une écriture peut réussir pendant que l'autre échoue — un problème de validation atomique (atomic commit), coûteux à résoudre. La racine du mal : il n'y a pas de leader unique déterminant l'ordre.

Change Data Capture : le log de la base comme flux

La capture des changements de données (Change Data Capture, CDC) observe toutes les écritures faites à une base et les extrait sous forme exploitable, idéalement en flux, dès l'écriture. En appliquant ce flux dans le même ordre à un index de recherche, on le maintient cohérent avec la base. En somme, CDC fait d'une base le leader (celle d'où l'on capture) et transforme les autres en suiveurs. Un broker à base de log convient parfaitement au transport, car il préserve l'ordre.

On peut implémenter CDC par des déclencheurs (fragiles, coûteux) ou en analysant le log de réplication (plus robuste). Bottled Water décode le WAL de PostgreSQL ; Debezium et Maxwell lisent le binlog de MySQL. Comme la messagerie, CDC est asynchrone : la base de référence n'attend pas les consommateurs, ce qui évite de la ralentir mais réintroduit le retard de réplication. Pour amorcer un nouveau système dérivé, il faut un instantané initial (initial snapshot) correspondant à un offset connu du log. Et pour libérer de l'espace sans perdre l'état, la compaction du log (log compaction) ne garde que la dernière écriture par clé : la taille dépend alors du contenu actuel de la base, non du nombre total d'écritures. On peut ainsi reconstruire une copie complète sans nouvel instantané.

Event sourcing : l'état dérivé d'un journal d'événements

Le sourcing d'événements (event sourcing), né dans la communauté DDD, ressemble à CDC mais opère à un niveau d'abstraction différent. En CDC, l'application manipule la base de façon mutable, et le log est extrait à bas niveau, à son insu. En event sourcing, la logique applicative est explicitement bâtie sur des événements immuables ajoutés à un journal ; le magasin d'événements est en ajout-seul, les mises à jour et suppressions sont proscrites, et les événements sont conçus pour refléter ce qui se passe au niveau métier.

Un journal seul ne suffit pas : l'utilisateur veut l'état courant (le contenu de son panier), pas l'historique. L'application doit donc rejouer le log par une transformation déterministe pour en dériver l'état lisible. La compaction diffère ici de CDC : un événement métier exprime l'intention d'une action (« ajouter au panier »), pas la nouvelle valeur d'un enregistrement ; les événements ultérieurs n'écrasent pas les précédents, et il faut l'historique complet pour reconstruire l'état. On stocke des instantanés pour accélérer, mais c'est une simple optimisation.

À retenir

L'event sourcing distingue soigneusement commande (command) et événement (event). Une requête entrante est d'abord une commande : elle peut encore échouer (une contrainte d'intégrité violée, un siège déjà réservé). L'application doit la valider synchroniquement. Une fois acceptée, elle devient un événement, durable et immuable — un fait. Un consommateur ne peut pas rejeter un événement : à ce stade, il fait déjà partie du log et d'autres l'ont peut-être vu.

État, flux et immuabilité

Toute donnée mutable est le résultat des événements qui l'ont fait évoluer : le solde d'un compte résulte des crédits et débits ; les sièges libres résultent des réservations. État mutable et journal immuable d'événements ne se contredisent pas : ce sont les deux faces d'une même pièce. Le journal des changements (changelog) représente l'évolution de l'état dans le temps. Comme le formule Pat Helland : « La vérité, c'est le log. La base de données est un cache d'un sous-ensemble du log. »

L'immuabilité est une vieille idée : les comptables tiennent un grand livre en ajout-seul depuis des siècles. Pour corriger une erreur, ils n'effacent pas — ils ajoutent une transaction compensatoire. Cela apporte l'auditabilité, facilite la récupération après un bug qui aurait écrit de mauvaises données, et capture plus d'information que le seul état courant (un article ajouté au panier puis retiré reste un signal analytique précieux). Surtout, en séparant l'état mutable du journal, on peut dériver plusieurs vues lisibles distinctes du même log — exactement comme plusieurs consommateurs d'un flux. C'est l'idée derrière la séparation des responsabilités commande/requête (Command Query Responsibility Segregation, CQRS) : la forme d'écriture des données n'a plus à être celle de leur lecture, et l'on peut dénormaliser librement les vues de lecture.

Traiter les flux

Une fois le flux en main, trois usages : l'écrire dans une base, un cache ou un index pour le requêter ensuite ; le pousser vers des humains (alertes, tableaux de bord temps réel) ; ou le transformer en un ou plusieurs flux de sortie. Ce dernier cas occupe la suite. Le code qui traite un flux s'appelle un opérateur (operator) ou une tâche (job) ; il consomme en lecture seule et écrit en ajout-seul, comme une tâche MapReduce. La différence cruciale : un flux ne finit jamais. Le tri n'a plus de sens, et une tâche tournant depuis des années ne peut pas « repartir du début » après une panne.

Parmi les usages : le traitement d'événements complexes (Complex Event Processing, CEP), qui cherche des motifs d'événements comme une expression régulière cherche des motifs de caractères (détection de fraude, trading). Fait notable : la relation requête/donnée est inversée — la requête est stockée durablement, et ce sont les événements qui défilent devant elle. Autre usage, l'analytique de flux (stream analytics) : agrégations et statistiques sur de nombreux événements (taux, moyennes glissantes, percentiles), souvent calculées sur des fenêtres temporelles, parfois via des algorithmes probabilistes (Bloom, HyperLogLog) économes en mémoire. Enfin, le maintien de vues matérialisées (materialized views) : maintenir caches, index et entrepôts à jour — un cas où il faut, non pas une fenêtre récente, mais une fenêtre remontant jusqu'au début des temps.

Raisonner sur le temps

« Les cinq dernières minutes » semble limpide, mais c'est trompeur. Un batch lit l'horodatage dans chaque événement, jamais l'horloge système — l'instant du traitement n'a rien à voir avec celui où l'événement s'est produit, et cela rend le traitement déterministe. Beaucoup de frameworks de flux, eux, utilisent l'horloge locale (le temps de traitement) pour fenêtrer : simple, mais faux dès qu'il y a un décalage notable.

Temps de l'événement (event time)Temps de traitement (processing time)
SourceHorodatage dans l'événementHorloge de la machine qui traite
DéterminismeOui (rejouable)Non
Sensible au retardRobusteFausse les résultats
RisqueHorloge client peu fiablePics artificiels après un redémarrage

Le retard a mille causes : pannes réseau, contention, redémarrage du consommateur, relecture après correctif. Pire, les messages arrivent dans un ordre imprévisible : un événement émis par le serveur B peut précéder celui de A bien qu'il soit survenu après. Kleppmann file l'analogie de Star Wars : sorti dans l'ordre IV, V, VI, puis I, II, III — l'ordre de visionnage (processing time) diffère de l'ordre narratif (event time). Si l'on mesure un taux de requêtes au temps de traitement, un redémarrage qui rattrape son retard produira un pic anormal alors que le débit réel était constant.

Fenêtres et événements en retard

Avec des fenêtres au temps de l'événement, on ne peut jamais être sûr d'avoir reçu tous les événements d'une fenêtre : un événement bufferisé ailleurs, retardé par une coupure réseau, peut surgir plus tard. Ces traînards (stragglers) imposent deux options : les ignorer (en comptant les abandons), ou recalculer la fenêtre et publier une correction. Un filigrane (watermark) peut signaler « plus aucun message antérieur à t », et les consommateurs attendent ce filigrane pour clore la fenêtre — sans garantie absolue si les horloges sont contrôlées par les clients. Pour ajuster une horloge d'appareil peu fiable (un mobile hors-ligne qui envoie des heures plus tard), on journalise trois horodatages : l'instant de l'événement selon l'appareil, l'instant d'envoi selon l'appareil, l'instant de réception selon le serveur — de quoi estimer le décalage.

Tumbling (fixe)   : [10:03:00–10:03:59] [10:04:00–10:04:59]  un événement → une fenêtre
Hopping (par saut): [10:03:00–10:07:59] [10:04:00–10:08:59]  longueur fixe, recouvrement
Sliding (glissante): tous les événements à moins de 5 min l'un de l'autre
Session            : événements rapprochés d'un même utilisateur ; clôt après 30 min d'inactivité

La fenêtre fixe (tumbling) range chaque événement dans exactement une fenêtre ; la fenêtre par saut (hopping), de longueur fixe, se recouvre pour lisser ; la fenêtre glissante (sliding) regroupe les événements distants de moins d'un intervalle ; la fenêtre de session (session), sans durée fixe, regroupe l'activité rapprochée d'un utilisateur et se ferme après une inactivité.

Jointures de flux

Comme en batch, on a besoin de jointures (joins), mais l'arrivée d'événements à tout instant les complique. On distingue trois types.

La jointure flux-flux (stream-stream, ou jointure par fenêtre) associe deux flux d'événements — par exemple une recherche et un clic partageant un ID de session, pour calculer un taux de clic. Le clic peut ne jamais venir, ou survenir des jours plus tard, voire avant la recherche à cause des délais réseau. On choisit une fenêtre (joindre si l'écart est inférieur à une heure) ; l'opérateur maintient un état indexé par session. La jointure flux-table (stream-table, ou enrichissement) associe un flux d'activité à une base (par exemple des profils utilisateurs). Plutôt qu'une requête distante lente, on charge une copie locale de la base, maintenue à jour par CDC : le processeur s'abonne au changelog des profils. La jointure table-table (table-table) associe deux changelogs : l'exemple de la timeline Twitter, un cache par utilisateur mis à jour à chaque tweet, abonnement et désabonnement — une vue matérialisée de la jointure de deux tables.

Piège courant

Les jointures sont sensibles au temps (time-dependent). Si un utilisateur modifie son profil, quels événements d'activité sont joints à l'ancien profil, lesquels au nouveau ? Dans une partition de log, l'ordre est préservé, mais pas entre flux ou partitions distincts. Si l'ordre inter-flux est indéterminé, la jointure devient non déterministe : rejouer la même tâche sur la même entrée peut produire un résultat différent, car les flux peuvent s'entrelacer autrement.

Tolérance aux pannes

En batch, la reprise est aisée : une tâche échouée redémarre ailleurs, sa sortie partielle est jetée, et tout se passe comme si chaque enregistrement avait été traité exactement une fois — la sémantique exactement-une-fois (exactly-once semantics). En flux, attendre la fin d'une tâche est impossible : le flux est infini. Il faut une reprise plus fine.

Une solution est le micro-batch (microbatching), employé par Spark Streaming : on découpe le flux en petits blocs (typiquement une seconde) traités chacun comme un minuscule batch. Plus petit, plus de surcharge ; plus grand, plus de latence. Apache Flink utilise des points de reprise (checkpoints) : un instantané périodique de l'état écrit sur stockage durable, déclenché par des barrières dans le flux ; après un crash, l'opérateur repart du dernier point et jette la sortie produite depuis. Dans les limites du framework, ces deux approches offrent la sémantique exactly-once. Mais dès que la sortie quitte le processeur (écriture en base, e-mail, message externe), le framework ne peut plus l'annuler : redémarrer ferait survenir l'effet de bord deux fois.

Pour donner l'apparence d'un traitement exactement-une-fois malgré les pannes, il faut que tous les effets d'un événement (messages en aval, écritures en base, changements d'état, avancée de l'offset) prennent effet si et seulement si le traitement réussit — atomiquement. C'est le problème de la validation atomique, qu'on peut traiter efficacement dans des environnements restreints (Google Cloud Dataflow) en écrivant la validation comme un objet unique dans un magasin tolérant aux pannes.

Astuce

L'autre voie, plus légère, est l'idempotence (idempotence) : une opération qu'on peut exécuter plusieurs fois avec le même effet qu'une seule. Écrire une valeur fixe dans un magasin clé-valeur est idempotent ; incrémenter un compteur ne l'est pas. On rend une opération idempotente en lui adjoignant des métadonnées : par exemple, stocker avec la valeur l'offset Kafka qui l'a déclenchée, pour détecter une mise à jour déjà appliquée. Cela suppose un rejeu déterministe dans le même ordre, et parfois du clôturage (fencing) pour écarter un nœud cru mort mais vivant.

Enfin, tout opérateur à état (agrégations fenêtrées, tables de jointure) doit pouvoir reconstruire son état après une panne. On peut le garder distant et répliqué (lent), ou — meilleure option — le garder local et le répliquer périodiquement (Flink vers HDFS ; Samza et Kafka Streams vers un sujet Kafka compacté). Parfois, l'état n'a même pas à être répliqué : il peut se reconstruire depuis les flux d'entrée, en rejouant les événements de la fenêtre concernée ou le changelog compacté.

À retenir

  • Le traitement de flux est le pendant non borné du batch : il traite des données incrémentales et sans fin, au fil de l'eau, pour réduire la latence ; un événement est un enregistrement petit, autonome et immuable d'un fait passé.
  • Deux familles de brokers : la file AMQP/JMS (un message à un consommateur, effacé après accusé, idéale pour des tâches à paralléliser) et le log partitionné (Kafka : offsets, ordre garanti dans une partition, conservation et relecture des messages).
  • Relier bases et flux : les écritures doubles sont dangereuses (incohérence silencieuse) ; CDC fait de la base un leader et de ses dérivés des suiveurs ; l'event sourcing bâtit l'état sur un journal d'événements métier immuables. État mutable et log immuable sont les deux faces d'une même pièce.
  • Raisonner sur le temps : ne pas confondre temps de l'événement (rejouable, robuste) et temps de traitement (sujet aux pics artificiels) ; gérer les traînards par filigranes, abandon ou recalcul ; choisir la fenêtre (fixe, par saut, glissante, session) selon le besoin.
  • Trois jointures de flux — flux-flux (par fenêtre), flux-table (enrichissement par CDC), table-table (vue matérialisée) — toutes à état et sensibles à l'ordre, donc potentiellement non déterministes.
  • La tolérance aux pannes vise la sémantique exactly-once par micro-batch, points de reprise, validation atomique ou idempotence ; l'état doit pouvoir être reconstruit, idéalement depuis un état local répliqué ou en rejouant les flux d'entrée.