Manuel d’ingénierie fintech
(w.pitula.me)- Les systèmes qui traitent l’argent comme état central doivent être conçus selon les principes suivants : ne pas créer de données, ne pas en perdre, et ne faire confiance à rien
- Pour représenter les montants, il faut éviter les float et combiner, selon les responsabilités,
BigDecimal, des entiers en unité minimale, des nombres rationnels, etc. ; la sérialisation des nombres JSON peut aussi recréer le problème des double IEEE-754 - Le grand livre doit permettre de reconstituer les soldes et les rapports au moyen de la comptabilité en partie double, d’une piste d’audit immuable, de la séparation entre value time, booking time et settlement time, ainsi que de l’enregistrement des corrections et annulations
- Les flux d’argent réels doivent empêcher les doubles dépenses et les omissions grâce aux réservations, à l’idempotence, à des machines à états redémarrables, à la validation des API externes et des webhooks, à l’outbox/CDC et au rapprochement (reconciliation)
- Le contrôle d’accès, l’approbation four-eyes, le suivi des changements dans le SDLC, les tests basés sur les propriétés et l’injection de pannes permettent de traiter les opérateurs internes et les modifications de code comme des frontières de confiance
Principes de base des systèmes fintech
- Dans l’ingénierie logicielle où l’argent est la principale préoccupation du système, la traçabilité, l’immuabilité et la vérifiabilité sont bien plus importantes qu’un CRUD classique
- Le public visé comprend les personnes qui rejoignent la fintech, celles qui y travaillent déjà, et celles qui, en dehors de la fintech, veulent comprendre en quoi les systèmes d’argent diffèrent des systèmes ordinaires
- Tous les patterns sont des moyens de respecter trois principes
- No invented data : l’argent ne peut pas être créé à partir de rien ; le traitement en double et les modifications arbitraires de solde ne sont donc pas autorisés
- No lost data : tout ce qui arrive à l’argent doit être tracé et persisté
- No trust : ne pas faire confiance aux fournisseurs externes, aux composants internes ni au monde réel ; tout vérifier
Représenter l’argent
- La représentation des montants est la décision la plus fondamentale dans un système financier ; si elle est mauvaise, toute la couche supérieure hérite des erreurs
- Les float/double peuvent entraîner des pertes de précision difficiles à prévoir et ne sont presque jamais un bon choix
- Ils ont toutefois l’avantage d’être rapides, économes en mémoire, et de ne pas nécessiter de bibliothèque ou de structure de données supplémentaire
- Les types à précision arbitraire comme
BigDecimalpermettent de contrôler explicitement la précision des calculs et l’endroit où l’arrondi est appliqué- Ils conviennent aux calculs intermédiaires comportant plusieurs opérations, comme le FX ou les calculs de prix
- Le stockage sous forme d’entier en unité minimale consiste à utiliser, pour la plupart des monnaies fiduciaires, la même précision fixe que les systèmes de banque centrale
- 12,34 € est stocké sous la forme
1234 - Il faut respecter le nombre de décimales défini par ISO 4217 et ne pas supposer qu’il y en a toujours 2
- Les cryptomonnaies utilisent aussi des entiers en unité minimale, comme le satoshi ou le wei, mais la précision varie selon l’actif et est définie par le token, comme avec
decimalsdans ERC-20 - Les montants en cryptomonnaie peuvent dépasser les entiers 64 bits, ce qui peut nécessiter des entiers de largeur arbitraire
- 12,34 € est stocké sous la forme
- Les nombres rationnels sont les plus puissants quand aucune perte de précision n’est acceptable, mais ils sont lents, difficiles à convertir vers d’autres formats sans perte de précision et nécessitent généralement un type ou une bibliothèque personnalisés
- Le mode de stockage et le mode de calcul sont des décisions distinctes ; un même système peut combiner un stockage en entiers avec des calculs intermédiaires en
BigDecimal - La gestion des frontières est aussi importante lors de la sérialisation des montants
- Dans la plupart des parseurs, un nombre JSON standard est un double IEEE-754 ; même si la représentation interne est prudente, le problème des float peut donc réapparaître aux frontières
- L’argent doit être envoyé sous forme de chaîne comme
"12.34"ou sous forme d’entier en unité minimale
Arrondi et gestion des devises
- L’arrondi est inévitable lors des divisions, conversions de devises, frais, intérêts, applications de taux et changements de précision ; il ne doit donc pas être laissé implicite
- La stratégie d’arrondi est une décision métier
- Il peut être nécessaire d’arrondir vers le bas par prudence, ou d’utiliser le half-even pour des effets statistiques
- La question de savoir qui récupère les fractions résiduelles peut avoir des implications juridiques et fiscales
- Il faut conserver la précision complète aussi longtemps que possible et n’arrondir qu’aux frontières, généralement avant le stockage ou l’affichage à l’utilisateur
- Si une valeur est divisée en plusieurs parties puis arrondie, la somme des parties peut différer de la valeur d’origine
- Selon le contexte, un compte d’arrondi explicite peut être nécessaire
- L’argent ne peut pas être représenté par un simple nombre ; il doit toujours être manipulé avec sa devise
- Regrouper le montant et la devise dans un newtype, struct, class ou record comme
Moneyréduit le risque d’erreur - L’addition de devises différentes doit être interdite, et les conversions doivent être effectuées explicitement avec des taux de change strictement contrôlés
- Il ne faut pas accepter des codes de devise arbitraires ; ils doivent être validés aux frontières du système par rapport à un ensemble de devises contrôlé
- Les codes de monnaies fiduciaires peuvent servir d’identifiants, mais les cryptomonnaies nécessitent une identification plus complexe, comme
(network, contract address) - Les cryptomonnaies pegged, bridged ou wrapped ne sont pas équivalentes à l’actif sous-jacent
- Regrouper le montant et la devise dans un newtype, struct, class ou record comme
Taux de change FX
- Un FX rate a toujours une direction
- Le taux EUR/USD n’est pas le simple inverse du taux USD/EUR
- Sur les places de marché, l’achat et la vente correspondent à des ordres à des prix différents en raison du bid/ask spread
- Le moment auquel s’applique le taux change aussi le résultat
- Le taux courant sert à calculer la valeur des positions actuelles ou d’une transaction supposée avoir lieu maintenant
- Le taux à value date sert à calculer les variations de valeur ou les montants fiscaux
- Deux types de taux sont importants dans les conversions
- Le Transactional rate est le taux auquel la conversion a réellement eu lieu ; il est déduit du montant d’origine et du montant résultant
- Le Reference rate sert aux évaluations et aux jugements d’équivalence, comme la valeur des positions ou la base fiscale ; ce n’est pas un prix de transaction réel
- Il n’existe pas de taux de change unique standard
- Les taux proviennent du marché et varient selon le lieu de négociation ou la méthode de calcul
- Les taux des banques centrales sont ce qui se rapproche le plus d’un standard, mais ne peuvent être utilisés que comme reference rate ; d’autres sources peuvent aussi être valides
- La source du montant et celle du reference rate doivent être conservées ensemble pour permettre une vérification ultérieure
Grand livre et comptabilité en partie double
- Les mouvements d’argent doivent être enregistrés d’une manière auditable et reconstituable même des années plus tard
- La comptabilité en partie double est une méthode largement utilisée qui stocke les transactions financières sous forme de liste d’entries
(credit account, debit account, amount)- La représentation classique utilise une ligne de débit et une ligne de crédit séparées pour chaque mouvement
- Comme chaque entry déplace le même montant d’un compte à un autre, le grand livre reste toujours équilibré
- L’argent a toujours une source et une destination
- Les fournisseurs externes doivent aussi disposer de comptes dédiés afin de suivre l’argent qui entre dans le système et en sort
- Les soldes ne sont pas stockés ; ils sont dérivés des mouvements d’argent
- Les comptes ont des types comme asset, liability ou equity
- L’accounting equation
assets = liabilities + equityest préservée - En pratique, des comptes revenue et expense sont aussi nécessaires pour enregistrer les revenus de frais ou les pertes de write-off
- La formule étendue est
assets = liabilities + equity + revenue - expenses
- L’accounting equation
- Une transaction génère généralement plusieurs mouvements
- Un mouvement du montant net et un mouvement des frais peuvent être créés séparément
- Par convention, une entry posted est immuable ; les corrections sont effectuées en ajoutant une nouvelle entry qui compense l’originale
Modèle temporel : value, booking, settlement
- Une transaction comporte généralement deux timestamps ou plus, parfois trois
- Value time : le moment où la transaction s’est réellement produite
- Booking time : le moment où elle a été enregistrée dans le système
- Settlement time : le moment où l’argent a été effectivement transféré ou matérialisé
- Le settlement time n’existe pas pour toutes les transactions et s’exprime souvent en T+X
- T+2 signifie que le settlement a lieu 2 jours après la value date
- Le value time et le booking time divergent presque toujours
- Si booking > value, la transaction est backdated, ce qui est particulièrement important lorsque la période de reporting change
- Si booking < value, elle est forward-dated, ce qui se produit avec les paiements planifiés ou les paiements à date future
- L’exemple d’un paiement par carte suit le flux suivant : le paiement a lieu à T1, le système l’enregistre à T2, puis le prestataire de paiement transfère l’argent sur le compte à T3
- Les rapports métier s’appuient principalement sur le value time ou le settlement time, tandis que le booking time est utile pour la traçabilité
- Fusionner plusieurs moments en un seul
created_atfait perdre des informations qu’il sera impossible de reconstituer plus tard
Piste d’audit, event sourcing, immuabilité
- Les systèmes financiers sont soumis à des audits réglementaires et peuvent devoir prouver, par exemple, si les fonds des utilisateurs et ceux de l’entreprise ont été mélangés, l’explicabilité des revenus, la cohérence entre les informations fournies à l’extérieur et la réalité, ou encore l’état de protection des fonds.
- Une audit trail est l’historique complet de la manière dont l’état actuel a été produit, et pas seulement l’état actuel.
- Ce qui s’est passé
- Quand cela s’est produit
- Qui ou quoi l’a déclenché
- Pourquoi cela s’est produit
- La piste d’audit est nécessaire non seulement pour les mouvements d’argent, mais aussi pour les interventions manuelles, les modifications de paramètres comme les fee schedules, rate sources ou limits, ainsi que les changements de permissions.
- Pour des décisions comme un compliance check ou un risk score, stocker uniquement le résultat peut être insuffisant.
- Si elles se trouvent dans une decision table ou un rules engine comme DMN, Drools ou Decisions4s, on obtient une structure rejouable indiquant quelle règle s’est exécutée avec quelles entrées et quel résultat elle a produit.
- L’event sourcing est une approche systématique pour créer une piste d’audit.
- Au lieu de stocker séparément l’état actuel et les logs, on ne stocke que les événements puis on en dérive l’état.
- Un ledger en comptabilité en partie double est un exemple de ce pattern, dans la mesure où il ne stocke pas le solde mais le calcule à partir des entries.
- L’event sourcing comporte aussi d’importantes contraintes en pratique.
- Il n’est pas nécessaire partout : si le ledger couvre déjà l’argent, les domaines périphériques peuvent se contenter d’un modèle classique et d’un change log fiable.
- Pour les performances, on peut mettre en cache ou snapshoter les balances et les projections.
- La source d’événements peut être difficile à interroger efficacement, ce qui peut entraîner beaucoup de travail de projection.
- Les événements restent conservés pendant des années, donc le code d’aujourd’hui doit aussi pouvoir lire des événements anciens.
- Une piste d’audit doit être append-only, car si elle est modifiable, elle ne constitue plus une preuve.
- Les outils possibles incluent des tables append-only, la suppression de
UPDATEetDELETEdans les permissions de la DB, le blocage des mutating operations dans la couche applicative, et la tamper evidence via des checksums ou une chaîne de hash.
- Les outils possibles incluent des tables append-only, la suppression de
- Dans les systèmes réels, il arrive qu’un bug impose de corriger un event log ou une piste d’audit.
- En général, une fois que les données ont été reportées à l’extérieur, elles doivent être figées ; avant reporting, il peut être possible de corriger sur place après avoir identifié le problème et avant qu’il ne sorte du système.
Annulations et corrections
- Des erreurs peuvent arriver, comme un posting avec un mauvais montant ou sur le mauvais compte.
- L’immuabilité impose de corriger en avançant.
- On poste une nouvelle compensating entry et on la relie bidirectionnellement au record original.
- Une reversal annule entièrement l’original sur le plan économique, comme s’il n’avait pas existé, mais l’original et la reversal restent tous deux dans l’historique.
- Une correction, ou adjustment, consiste à booker l’écart entre l’enregistrement réel et la bonne valeur, ou à annuler puis à reposter avec la bonne valeur.
- Une correction peut tomber dans une période de reporting différente de celle de l’original.
- Les informations de liaison sont nécessaires pour que les rapports attribuent correctement la correction et distinguent l’activité réelle du cleanup.
- Comme il n’est généralement pas permis de backdater une période de reporting déjà clôturée, le choix d’indiquer une value time passée lors d’une correction dépend du calendrier de reporting.
Exécution des flux d’argent : invariants et réservation de fonds
- Un invariant est une propriété qui doit toujours être vraie dans le système.
- L’équation comptable en est un exemple, et les parties prenantes métier peuvent définir plusieurs conditions.
- Les méthodes pour imposer des invariants sont complémentaires.
- Concevoir le système pour ne créer que des objets valides dès la phase de création.
- Vérifier à l’exécution avec des assertions, des tests et du property-based testing.
- Analyser a posteriori les données stockées avec des jobs de reconciliation ou des contrôles nocturnes.
- Les transactions qui interagissent avec le monde extérieur doivent éviter les race conditions.
- Il faut empêcher les situations où l’on découvre l’insuffisance de solde seulement après un appel externe, ou où le même argent est dépensé deux fois.
- La funds reservation, ou hold-and-release, est un pattern consistant à réserver des fonds pour une transaction donnée avant une interaction externe.
- En cas de succès, on settle la reservation et on poursuit la transaction.
- En cas d’échec, on la release et on la renvoie dans l’available balance.
- Ce pattern distingue le total balance de l’available balance.
available = total - reserved- La vérification du solde et la nouvelle reservation se font sur la base de l’available balance.
- Le montant final peut différer de l’estimation initiale.
- Si les frais ou le taux de change changent, on réserve le montant estimé, on settle le montant réel puis on release le reste.
- Une reservation doit impérativement être settle ou release.
- Une reservation orpheline bloque les fonds de l’utilisateur, mais ne perd ni ne crée d’argent.
- Une expiry ou un timeout peuvent servir de filet de sécurité, mais ne sont pas indispensables.
- La vérification du solde et l’enregistrement de la reservation doivent être linearizable.
- Avec une stale read, deux transactions peuvent toutes deux passer la vérification et être adossées aux mêmes fonds.
Overdraft et idempotence
- Un overdraft est une situation où le solde d’un compte devient négatif.
- Un overdraft intentionnel est un produit de crédit avec limite et intérêts, généralement modélisé comme un compte d’overdraft séparé.
- Un overdraft non intentionnel peut survenir même s’il est interdit par la politique.
- Un settlement peut arriver avec un montant supérieur à l’estimation réservée, ou une reversal peut arriver après que les fonds ont déjà été sortis.
- La funds reservation réduit la fenêtre, mais ne la supprime pas.
- « Interdit » et « impossible à représenter » sont deux choses différentes.
- Si l’on rend les soldes négatifs impossibles à représenter avec un entier non signé ou
CHECK (balance >= 0), cela peut mener à un crash, à un clamp à zéro ou à un traitement incorrect lorsque, dans la réalité, il faut accepter un solde négatif. - Clamper un solde négatif à 0 crée de l’argent.
- Si l’on rend les soldes négatifs impossibles à représenter avec un entier non signé ou
- Lorsqu’un overdraft est détecté, il faut le traiter comme un signal d’investigation, puis le recouvrer ou le traiter explicitement par des moyens comme un futur deposit avec netting, une demande de remboursement, ou un write-off vers un compte d’expense/loss.
- Dans les systèmes distribués, il est impossible de garantir une exactly-once delivery ; les retries sont donc nécessaires, et les retries peuvent créer des livraisons en double.
- L’idempotency est la propriété selon laquelle, même si le même message est livré deux fois, le traitement ne produit l’effet qu’une seule fois.
- Une idempotency key explicite est généralement plus simple et plus sûre qu’une deduplication basée sur le payload.
- La key doit être limitée au périmètre d’une operation et d’un client donnés.
- Il faut décider s’il faut rejouer les erreurs ou retraiter ; pour les erreurs permanentes, il est généralement plus simple de les rejouer telles quelles.
- Vérifier que le payload d’un appel dupliqué est identique à l’original est une bonne pratique, mais cela a un coût en complexité d’implémentation et en flexibilité.
- À grande échelle, la deduplication de dizaines de milliards de requêtes et un atomic barrier en cas d’accès concurrents sont nécessaires.
- Une idempotency window de 24 heures, par exemple, simplifie l’implémentation mais a un coût en correctness.
- Il faut aussi tester les retries et gérer les retries out-of-order.
Flux redémarrables
- Un flux d’argent s’étale sur plusieurs étapes, et il faut supposer qu’il peut mourir n’importe où entre ces étapes.
- La full resumability est une conception qui garantit qu’un flux à moitié terminé se trouve toujours dans un état récupérable.
- L’état d’avancement doit être stocké dans un stockage persistant, pas en mémoire.
- Il faut modéliser le flux comme une state machine explicite et commit l’achèvement de chaque étape avant de démarrer la suivante.
- Un driver indépendant est nécessaire pour relancer les flux interrompus.
- Un scheduler, worker ou poller doit traiter les incomplete flows même après un crash de l’orchestrator.
- Lors d’une reprise, des étapes déjà partiellement exécutées peuvent être exécutées à nouveau ; chaque étape doit donc être idempotente.
- Les effets externes ne peuvent pas être rollback.
- Après avoir appelé le monde extérieur, il est impossible de revenir à l’état où l’appel n’avait pas été fait.
- Il faut soit roll forward jusqu’à la complétion, soit, si une étape ultérieure échoue définitivement, poster une compensating action comme dans le saga pattern.
- On peut utiliser un moteur de durable execution comme Temporal, Camunda, Workflows4s ou AWS Step Functions, ou construire soi-même une persistent state machine.
Consommation d’API externes
- Les API externes comme celles des fournisseurs de paiement, des custodians, des nœuds blockchain ou des prestataires KYC doivent être traitées de façon défensive, car on ne contrôle ni leur code, ni leur qualité, ni leur disponibilité.
- Il ne faut pas faire confiance au schéma.
- Des champs peuvent manquer, des types peuvent changer, des
nullinattendus peuvent apparaître. - Les parties importantes doivent être validées à la frontière, et les données inattendues doivent provoquer un échec explicite.
- Valider aussi les parties inutiles peut créer des incidents évitables à cause d’une violation de contrat par un tiers.
- Des champs peuvent manquer, des types peuvent changer, des
- Avec les API externes, il est tout à fait possible de rencontrer des tokens transmis dans l’URL, des pertes de précision, des codes HTTP qui ne correspondent pas à leur signification, un corps d’erreur dans un
200, une pagination incohérente ou des formats de date personnalisés. - Tout appel peut échouer ; il faut donc des timeouts et des retries.
- Un circuit breaker est surtout une marque de courtoisie envers un serveur surchargé, et augmente la complexité côté client.
- Il peut toutefois être nécessaire pour protéger la latence et des ressources finies comme les threads ou les connexions.
- Pour les rate limits et les quotas, il faut estimer à l’avance le volume d’appels prévu et le comparer aux limites du provider.
- Stocker toutes les requêtes et réponses sous une forme structurée et interrogeable fournit de la matière pour les enquêtes, un audit trail, des preuves en cas de litige sur le comportement du provider, ainsi que des données pour retraiter après un bug.
- Dans les domaines critiques, on peut envisager une redondance de providers.
- Cela peut prendre la forme d’une vérification des données via deux nœuds blockchain, d’un partenaire bancaire de secours, d’un crypto custodian ou d’un prestataire KYC alternatif.
- Les coûts de développement, de frais et de complexité sont très élevés.
- Un sandbox peut différer fortement de la production ; il faut donc préparer les tests en production via des canary releases ou des usages contrôlés à faible impact.
Traitement des webhooks
- Les webhooks sont un moyen courant de recevoir des signaux de systèmes externes, mais les traiter en sécurité n’est pas simple.
- Il ne faut pas supposer l’ordre.
- Les messages peuvent arriver out-of-order ou contenir des données obsolètes.
- Il ne faut pas considérer que le webhook qui vient d’être reçu est la vérité la plus récente et écraser l’état avec lui.
- Il ne faut pas supposer la validité.
- Les webhooks proviennent d’autres sous-systèmes de l’émetteur et peuvent contenir des données obsolètes ou mal transformées.
- Il est préférable d’utiliser le corps du webhook uniquement comme trigger, puis d’interroger l’API pour confirmer l’état faisant autorité.
- L’API peut elle aussi être eventually consistent : une requête immédiate peut renvoyer l’ancien état, des retries sont donc nécessaires.
- Il ne faut pas supposer la livraison.
- Même si l’émetteur promet une politique de redelivery robuste, un webhook finira un jour par être perdu.
- Un processus indépendant, comme une reconciliation, doit compléter l’intégrité des données.
- Il ne faut pas non plus supposer une livraison unique.
- Le même webhook est livré plusieurs fois, et le traitement doit être idempotent.
- Il faut acknowledge rapidement et traiter de façon asynchrone.
- On stocke l’événement brut dans un durable store, puis on renvoie immédiatement un 2xx ; le travail réel est effectué de façon asynchrone.
- Le payload brut doit être stocké tel quel.
- C’est nécessaire pour un traitement fiable, l’audit trail et le retraitement après un bug.
- L’appelant doit être vérifié.
- En général, l’émetteur ajoute une signature au payload, et le récepteur la vérifie avec un HMAC basé sur un shared secret ou une signature asymétrique à clé publique.
- La vérification de signature doit se faire sur les raw bytes reçus, et non sur un payload resérialisé.
- Les webhooks doivent être traités comme un indice que quelque chose s’est produit, et non comme la vérité sur ce qui s’est produit.
Notifications fiables : Outbox et CDC
- Lorsqu’il faut notifier de façon fiable un changement de système via un canal externe, comme un événement Kafka ou un appel webhook, la transactionnalité devient un problème.
- Il peut arriver que la publication réussisse mais que la réponse ne soit pas reçue à cause d’un problème réseau, entraînant un rollback de l’état du système ; ou que le changement d’état soit commit, mais que la publication échoue.
- La réponse classique des manuels est le 2-phase commit ou la transaction distribuée, mais ils sont rarement utilisés à cause de leur complexité et de la difficulté à standardiser leur réutilisation.
- Il existe plusieurs choix pragmatiques.
- Outbox pattern : enregistrer transactionnellement l’intention de publication dans un store dédié en même temps que le changement d’état, puis la traiter plus tard jusqu’à réussite.
- Change Data Capture : lire le write-ahead log ou le journal de réplication de la base de données pour transformer les changements commités en event stream.
- Debezium et AWS DMS fournissent du CDC.
- Le CDC émet des événements bruts sous forme de lignes de table ; un post-traitement est donc nécessaire pour éviter de faire fuiter le schéma interne.
- Le listen-to-yourself consiste à publier d’abord un événement, puis à reconstruire son propre état à partir de cet événement.
- Avec l’event sourcing, l’event log est déjà dans la DB : il suffit donc de publier depuis celui-ci.
- Quel que soit le mécanisme choisi, la livraison est at-least-once.
- Si un relay ou un connector crashe après la publication mais avant l’enregistrement, il peut renvoyer l’événement au redémarrage.
- Le consumer doit dédupliquer avec un stable event id et se comporter de manière idempotente.
Reconciliation
- Les systèmes qui dépendent de données externes sont vulnérables au data drift, lorsque l’état de deux systèmes diverge.
- Un webhook peut être manqué, ou une transaction peut être postée dans le ledger sans être reflétée dans le système du provider externe.
- La reconciliation est le processus qui aligne deux systèmes.
- En pratique, il peut y en avoir trois ou plus, comme un ledger, un payment processor et une banque.
- La cadence peut être horaire, quotidienne, mensuelle ou annuelle selon le contexte et les contraintes.
- Le drift peut être une donnée manquante, ou une différence plus complexe, par exemple un montant différent pour la même transaction.
- Le timing est également important.
- Si le settlement est à T+3, un record peut rester unreconciled pendant trois jours ; il faut le refléter dans le processus pour éviter des alertes inutiles.
- L’algorithme de matching est la principale difficulté.
- En général, stocker en interne l’identifiant du provider externe simplifie le matching.
- Sinon, des heuristiques basées sur le montant et l’heure peuvent être nécessaires.
- Une reconciliation one-to-many peut aussi être nécessaire.
- Un seul transfer de settlement peut couvrir plusieurs transactions.
- Il ne faut pas simplement écraser une discrepancy pour que la reconciliation tombe juste.
- Il faut comprendre et corriger la cause avec un support de premier ordre pour les correction records, le retraitement des données de webhook, etc.
Contrôles et accès
- Les systèmes d’argent doivent contrôler non seulement les données, mais aussi qui peut effectuer quelles actions, et prouver a posteriori que les procédures ont été respectées.
- La Segregation of duties est un contrôle qui empêche une seule personne de posséder l’ensemble du processus.
- Le four-eyes, le maker-checker et le dual control sont des approches où une deuxième personne doit approuver une action avant son application.
- Cela s’applique aux actions qui peuvent déplacer des fonds ou les afficher incorrectement, comme les retraits importants ou manuels, les corrections manuelles de ledger, les mouvements de trésorerie et de cold wallet, ou les modifications de fee schedule et de limites.
- Les mêmes contrôles s’appliquent à l’ingénierie.
- Les code merges, les déploiements en production et les changements d’infrastructure sont des actions sensibles dans les systèmes d’argent ; ils nécessitent donc review et approval.
- L’approval lui-même fait partie du trail.
- Il faut enregistrer qui a demandé, qui a approuvé, et si les deux personnes étaient différentes pour pouvoir prouver le contrôle.
- Pour les urgences, il faut un chemin break-glass explicite et fortement audité.
- L’access control fait partie de l’état du système et évolue avec le temps.
- Les humains comme les services doivent recevoir le minimum de privilèges.
- Préférer le RBAC aux grants par personne facilite les reviews.
- Les capability grants et les revocations sont aussi des événements sensibles ; il faut donc enregistrer ce qui a changé, qui l’a changé et pourquoi.
- Des scheduled access reviews doivent détecter la permission drift, lorsque les permissions deviennent anciennes ou inexactes.
Suivi des changements dans le SDLC
- Dans un environnement réglementé, il faut pouvoir auditer le cheminement du code jusqu’à la production
- Le source control constitue l’historique des changements
- L’historique des commits attribue chaque changement à un auteur et le relie à sa raison via la review et le ticket associé
- Il doit être protégé par des commits signés, des branches protégées et l’interdiction des force-push sur l’historique partagé
- Les reviews et le pipeline doivent être imposés
- Les reviews obligatoires, les status checks et l’interdiction des push directs sur la branche main sont importants
- Le deployment doit être traçable
- Il faut pouvoir reconstituer quelle version est en cours d’exécution, qui l’a release et quand, afin de relier un incident au changement qui en est la cause
Stratégie de test
- Dans les systèmes d’argent, l’espace des séquences d’opérations est vaste et les défaillances intéressantes émergent des combinaisons ; les tests sont donc particulièrement importants
- Le Property-based testing vérifie des propriétés qui doivent rester vraies pour toute entrée, plutôt qu’une sortie particulière
- Il convient bien aux invariants et au calcul monétaire
- Lorsqu’on génère des séquences d’opérations, il faut vérifier les invariants après chaque étape, pas seulement à la fin
- Comme il est difficile de le faire manuellement à grande échelle, il faut un testing harness qui injecte automatiquement les assertions
- Le generative idempotency testing vérifie que toute opération qui touche le monde extérieur n’a aucun effet lors d’un second appel
- L’injection de crash and resume vérifie qu’un long flow peut reprendre même s’il s’arrête entre deux étapes quelconques
- Le round-trip testing vérifie qu’après encode/decode, serialize/deserialize ou convert/convert back, on revient au point de départ ou que l’on reste dans une tolérance connue
- C’est un moyen rapide de détecter les pertes de précision aux limites des types money et currency, ainsi que les bugs de serialization
- Le golden testing compare des résultats de calcul, comme des détails de frais, des statements ou des reports, à des résultats attendus enregistrés, afin de révéler des diffs involontaires
- Le backward-compatibility testing conserve un corpus d’anciens payloads au format réel et vérifie que le code actuel les deserialize et les project toujours correctement
- Les tests en production peuvent être nécessaires lorsque le sandbox diffère fortement de la production
- Exemples : canary release, controlled rollout à faible blast radius, synthetic transactions faisant circuler en continu de petits montants réels
- Les tests en production déplacent de l’argent réel ; ils doivent donc passer par le même ledger, la même reconciliation et le même audit trail, puis être nettoyés via les chemins normaux de correction/reversal
Terminologie du domaine et ressources de référence
- Dans une introduction à la fintech, le vocabulaire et les concepts peuvent être plus difficiles que le code ; les termes clés sont donc regroupés séparément
- Le domaine de la comptabilité et des ledgers inclut ledger, general ledger et sub-ledger, debit/credit, posting, chart of accounts, receivable/payable, IOU, accrual vs cash basis, trial balance, suspense/clearing account, write-off, commingling, reconciliation break
- Le domaine Money et FX inclut Money type, minor units, basis point, notional, fiat vs crypto, stablecoin, pegged/wrapped/bridged, bid/ask/spread, mid-market rate, reference rate, mark-to-market
- Le domaine des transactions et du settlement inclut value date, booking date, settlement date, T+X, clearing vs settlement, cut-off time, float, netting, backdating, reversal/correction
- Les termes relatifs aux paiements, aux cartes, aux marchés, à la crypto et à la compliance sont également regroupés séparément
- PSP, omnibus account, FBO account, chargeback, issuer/acquirer, authorization vs capture
- order book, market vs limit order, maker/taker, slippage, liquidity, derivative, futures, perpetual, liquidation
- custody, hot/cold wallet, private key, multisig, MPC, gas, confirmation/finality, reorg, UTXO vs account model
- KYC, AML/CFT, sanctions screening, PEP, SoF/SoW, Travel Rule, VASP, MiCA, least privilege, RBAC, audit trail
- Les ressources de référence sont réparties entre comptabilité et ledgers, payments et cards, markets et trading, crypto, engineering, KYC et AML
- Accounting for Computer Scientists : un article pour ingénieurs qui explique la comptabilité en partie double à travers des graphes et des modèles de données
- Modern Treasury, How to Scale a Ledger : une série d’articles qui aborde les ledgers de production sous l’angle du software engineering
- Designing Data-Intensive Applications : traite de l’idempotency, des logs, de la consistency et des failure modes du point de vue des systèmes
Trois exemples end-to-end
-
Retrait crypto
- Flux dans lequel l’utilisateur retire 0,5 ETH vers une adresse externe
- La requête inclut une idempotency key afin que des soumissions en double ne créent qu’un seul retrait
- 0,5 ETH et les network fees estimés sont réservés sur le solde disponible
- Le compliance gate vérifie les sanctions, l’AML et l’adresse de destination, et peut rester en sleep pendant plusieurs jours en raison d’appels externes et d’une review manuelle
- Le broadcast on-chain doit être idempotent ; après un crash, il faut revérifier la blockchain plutôt que d’effectuer un second broadcast
- Après suffisamment de confirmations, on enregistre dans le ledger les postings suivants : débit du compte utilisateur, crédit du compte on-chain externe, charge de network fee et produit de service fee
- Un job nightly réconcilie le ledger avec la réalité de la blockchain
-
Dépôt par carte
- Flux de recharge par carte via un PSP
- L’utilisateur soumet le montant et les informations de carte, puis ouvre une deposit transaction auprès du PSP avec une idempotency key
- L’authorization ne crée qu’un hold ; l’argent n’appartenant pas encore à l’entreprise, on ne crédite pas le solde utilisateur
- Le webhook
capturedvérifie la signature sur les raw bytes, stocke le raw payload, répond rapidement en 2xx, puis est traité de façon asynchrone - Le webhook n’étant qu’un trigger, l’état faisant autorité est récupéré depuis l’API du PSP
- L’état captured but not settled est enregistré via un clearing account, et le settlement peut arriver en batch à T+X
- Un chargeback est traité par une écriture compensatoire liée, sans modifier l’écriture d’origine
-
Conversion in-app avec cashback
- Flux dans lequel 1 000 EUR sont convertis en USDC avec un cashback promotionnel
- Le quote EUR→USDC n’est pas l’inverse de USDC→EUR : c’est un taux directionnel
- L’EUR et l’USDC ne s’additionnent pas entre eux ; l’USDC est identifié par
(network, contract address)et n’est pas équivalent à une fiat adossée - Les calculs conservent toute la précision, et l’arrondi n’est effectué qu’une seule fois à la frontière, selon une stratégie explicite
- Le spread doit être explicitement booking dans un compte de revenus et ne doit pas disparaître sous forme de rounding residual
- Le cashback n’est pas une hausse gratuite du solde, mais de l’argent réel déplacé d’un compte promotionnel/de dépenses de l’entreprise vers le solde utilisateur
- La publication du résultat garantit une reliable delivery via un mécanisme comme outbox, CDC ou event log, et les consumers downstream dédupliquent avec un stable event id
1 commentaires
Avis sur Hacker News
Je l’ai parcouru, et je trouve que ce manuel est superficiel et, dans certains domaines, frôle le mauvais conseil.
Par exemple, si je vois des montants stockés sous une forme non entière, j’aurais envie de m’enfuir en hurlant. À cause de choses comme le decimal de Rust représenté en JSON sous forme de nombre à virgule flottante. Sauf raison très solide, il faut toujours utiliser des entiers, et les vues exportées peuvent ensuite prendre n’importe quelle forme, même un format bizarre d’encodage de bits
Les taux de change ne sont pas non plus un problème qui se résout avec un seul instant donné. Le taux au moment de référence de l’acheteur, celui du vendeur, l’accord, la tolérance d’accord, l’horodatage définitif convenu, tout cela joue
À cause de l’immutabilité, dès qu’on manipule de l’argent, on a envie d’utiliser de l’event sourcing. Le flux final consolidé peut ressembler à
A -> B -> E, mais le flux réel peut êtreA0 -> Edit(A0, A) -> B -> C -> D -> Rollback(B) -> EAu fond, toutes les fintechs ne se valent pas. À certains endroits, l’argent était traité comme de la marchandise en vrac ; à d’autres, il était au centre de tout
Pour le change, cela semble plutôt renforcer l’idée du manuel selon laquelle « il n’existe pas de taux canonique ». En plus, le texte traite des enregistrements après finalisation, alors qu’ici il me semble qu’on parle de la méthode de finalisation. C’est une nuance valable pour un objectif distinct, mais je ne vois pas en quoi cela prouve une omission ou une erreur
Sur la partie immutabilité aussi, le texte me semble dire la même chose. Je ne vois pas ce qui est différent
Si vous calculez par Monte Carlo le prix d’options sur une trajectoire de taux, et que vous vous intéressez à des mesures de risque comme la duration, la convexité ou le véga, personne ne se soucie des règles d’arrondi. Un double suffit. Comment comptez-vous forcer
exp(-rt)cashflowou la fonction de répartition normale cumulée à être des entiers ?Il y a des domaines où les entiers sont adaptés. Mais ce n’est pas un principe universel : il faut faire le bon choix d’ingénierie
Si l’environnement utilisé le prend en charge, on peut aussi utiliser du fixe, mais techniquement cela reste des entiers
Pour l’affichage, il est sûr de renvoyer des valeurs décimales
Fuir un système qui stocke les montants sous forme d’entiers, très bien. Comme ça, on ne travaillera probablement pas sur le même système. De nos jours, j’ai plutôt souvent envie de fuir les systèmes qui traitent les montants comme des entiers. Dans une base de code idéale, manipulée uniquement par des programmeurs financiers expérimentés, cela peut bien fonctionner, mais ce genre de système risque souvent de devenir trop exclusif ou fragile
À ceux qui envisagent une stratégie de précision en unités mineures pour représenter les montants, je conseillerais de ne pas le faire. Au minimum, il ne faut pas l’utiliser comme format de données d’échange/API
Cela peut sembler malin — opérations entières rapides, pas de problèmes d’arrondi à l’addition et à la soustraction —, mais dès qu’on travaille avec un partenaire qui n’a pas les mêmes hypothèses implicites sur le nombre de décimales pour une devise donnée, on peut se faire très sévèrement piéger. C’est particulièrement important pour les stablecoins, qui ont souvent un nombre implicite de décimales différent de celui de la monnaie fiduciaire qu’ils représentent
Dans les API basées sur JSON, il vaut aussi la peine d’envisager de représenter les montants sous forme de chaînes. JSON ne spécifie pas la précision décimale, donc il faut toujours vérifier que votre parseur/sérialiseur, ainsi que ceux de tous vos utilisateurs/fournisseurs, ne passent pas en interne par des nombres à virgule flottante en perdant de la précision. Ça peut vite devenir sale, et les chaînes paraissent conceptuellement moins propres, mais cela contourne complètement le problème. Certains diront que c’est un antipattern [1], mais je n’ai pas envie de mener ce combat pour une pureté idéologique sur le dos des utilisateurs ou des actionnaires
[1] https://blog.json-everything.net/posts/numbers-are-numbers-n...
Dans le trading haute fréquence, si l’on peut fixer à l’avance un exposant cohérent pour une certaine {slice}, on peut économiser de la place à la transmission. Par exemple, pour un périmètre comme produit/taille de tick/classe d’actifs/bourse/flux/serveur, on n’envoie que la mantisse et le client utilise un exposant codé en dur
Mais même dans des domaines similaires, il vaut souvent la peine d’envoyer un exposant
uint32supplémentaire dans les données transmises. Cela permet de le changer plus tard et de ne pas se retrouver bloqué par une conception initiale du type « pour l’instant, on n’a besoin que des cents ». Par exemple, on peut soudain devoir prendre en charge le prix du bitcoin avec toute sa précision. Les utilisateurs vous seront reconnaissants de ne pas avoir à coordonner un changement cassant au moment où vous voudrez ajuster l’exposant fixeLe critère « n’importe quel » est déraisonnable. C’est une norme impossible à atteindre, qui peut exiger un budget d’ingénierie illimité sans valeur réellement démontrable
Identifier l’absence de standard, expliquer comment les parseurs réels se comportent, discuter des lacunes et des cas d’usage non couverts, c’est très bien. Proposer qu’il faudrait un standard plus raisonnable, c’est très bien aussi. Mais demander à tout le monde de prendre en charge « toutes les possibilités », alors que personne n’en a réellement besoin, que le sens est flou et que c’est impossible à réaliser en pratique, ce n’est pas une bonne idée
float/double, de l’arithmétique à virgule fixe au millième de l’unité mineure ou plus petit, des décimaux à précision arbitraire, ou une approche complètement différente ?En tant que programmeur, quand je vois des programmeurs Fintech parler chacun depuis des expériences et des points de vue différents, je me demande ce que signifie vraiment bien programmer
Quand xlii dit de ne pas stocker les montants en virgule flottante, c’est le problème classique d’IEEE 754. Le suivi financier devrait se faire avec des journaux immuables ou des enregistrements fondés sur les événements, mais je ne pense pas qu’il faille transformer tous les services environnants en event sourcing. L’appliquer uniquement à la logique cœur — grand livre, règlement, ordres, exécutions — me semble suffisant. À lire xlii, on dirait une technique qui ne devient possible que lorsque la modélisation a réussi
lxgr pointe le problème des unités mineures. Si un nombre JSON est parsé par le langage ou le parseur en virgule flottante, de la précision peut être perdue. En général, on envoie la valeur avec un champ séparé pour le nombre de décimales. Cela dit, j’ai entendu dire qu’en trading haute fréquence, ce surcoût lui-même est trop coûteux, donc on ne fait pas comme ça
Ce que dit antonymoose rejoint ce que l’on trouve dans beaucoup de livres. C’est pourquoi ce type de conception est courant dans le contexte du change ou des API. Cela ressemble aussi à de la conception de protocole
Au final, tout le monde a raison dans son propre domaine. Je me dis que ce serait bien d’avoir quelqu’un comme xlii comme programmeur senior, tout en ayant aussi l’impression que je ne serais pas capable de concevoir un système aussi complexe. En ce sens, les propos de chacun sont valables, et il est intéressant de voir les avis diverger selon le domaine. Je me demande si c’est ça, l’expertise
En voyant ce genre de choses, on peut à peu près deviner de quelles expériences vient un programmeur. Parfois, la programmation ressemble moins à la recherche de la bonne réponse qu’au choix d’une vision du monde
Sur HN, il est toujours intéressant de voir comment les programmeurs modélisent leur domaine. Parfois je clique sur leur profil et j’ajoute leur connaissance métier à mon wiki personnel, en me disant que ça servira peut-être un jour
Bien. Ce livre contient déjà beaucoup de bonnes informations qu’on peut trouver ailleurs, mais le simple fait de les rassembler le rend assez pratique. Je recommande vivement Designing Data-Intensive Applications de Kleppmann. La première édition était déjà excellente, et la deuxième est sortie récemment
J’ai travaillé comme CTO dans la FinTech et construit toute la pile logicielle à partir de zéro ; les leçons du livre sont globalement justes. Je dis globalement parce que, comme toujours, pour un projet donné, il faut beaucoup tenir compte du « ça dépend ». Par exemple, je n’ai pas utilisé l’event sourcing afin d’éviter le problème du calcul de l’état complet. Une piste d’audit standard en append-only peut très bien suffire
On ne peut pas garantir une livraison exactement une fois, mais on peut mettre en place un traitement effectivement une fois, et c’est en réalité ce que l’on veut
L’idée de stocker toutes les requêtes et réponses est absolument juste. Il faut le faire non seulement quand on consomme une API, mais aussi quand on collecte des informations depuis le monde extérieur ; et si possible, il faut aussi enregistrer toutes les transformations intermédiaires à l’intérieur de la frontière. Une combinaison de buckets adressés par contenu et de tables relationnelles fonctionne bien
Par ailleurs, le texte ne dit rien de la lignée des données. Que faire si un fournisseur met à jour des données en plein milieu de la journée et que vous devez absolument le savoir ? Il faut pouvoir expliquer ce changement tout en faisant en sorte que relancer un calcul avec les anciennes valeurs produise le même résultat. Ce n’est pas un problème particulièrement difficile à résoudre, mais il demande de la réflexion
Dire que ce sont de « mauvais conseils » est une formulation plutôt polie. Honnêtement, ce « handbook » semble avoir été écrit en grande partie par un LLM
Par exemple, dans la section sur l’immuabilité, on trouve cette phrase : « Séparer les PII des données financières permet de respecter le droit à l’effacement sans perdre l’historique financier qui doit être conservé »
Dans les institutions financières, les deux vont clairement de pair pour des raisons de KYC/AML
Si vous supprimez immédiatement, sur demande, le nom, l’adresse, etc. d’un client avant l’expiration de la période applicable, tout en ne conservant que les données financières, toute l’organisation va passer une très mauvaise journée le jour où une autorité légitime viendra demander les données pour retracer une activité criminelle
Les personnes qui veulent travailler dans la Fintech ne devraient pas s’appuyer sur un « handbook » arbitraire écrit par une personne inconnue dans une juridiction inconnue
Les personnes qui travaillent dans la Fintech ne devraient travailler qu’en suivant le handbook/les directives internes de leur employeur, etc. Ces documents auront été rédigés conjointement par les avocats de l’entreprise et les responsables conformité, afin de satisfaire aux lois et aux obligations de reporting des juridictions dans lesquelles l’employeur opère
À mes yeux, il recommande plutôt de séparer les PII qui devront finalement être supprimées des données que l’on souhaite conserver pratiquement indéfiniment, parce qu’elles font partie de l’équation comptable/des invariants. Cela permet de supprimer les premières une fois la période de conservation applicable écoulée
Il est vrai qu’il ne faut pas s’appuyer sur un « handbook » écrit par une personne inconnue dans une juridiction inconnue. Mais il ne faut pas non plus ignorer aveuglément les idées et pratiques qui y figurent, ni refuser de regarder au-delà de sa propre organisation. Idéalement, on devrait les examiner puis les confronter à ses propres connaissances et aux réglementations locales pour les adapter
Dans un monde où seules existent des organisations parfaites et sans erreurs, l’approche consistant à suivre uniquement les directives internes de son employeur paraît raisonnable. Mais comment atteindre ce niveau sans ce genre de discussion ?
Je pense que l’essentiel de ce contenu s’applique non seulement à la Fintech, mais à l’ingénierie logicielle en général
Par exemple, les parties sur les reprises, l’idempotence, l’ordre des événements, etc. s’appliquent à tout système qui exige un certain degré d’exactitude, même quand l’argent n’est pas directement en jeu. J’ai vu trop de systèmes construits sur l’hypothèse qu’« on peut toujours réessayer », alors qu’une reprise n’est possible que si l’échec est propre dès le départ et que les sous-systèmes fournissent le niveau d’idempotence que je crois qu’ils fournissent. Ces points sont rarement vérifiés en pratique
J’aimerais plutôt lire un article défendant une approche plus radicale, comme une base de données par compte. Quelque chose avec des compromis propres à la Fintech
Le principal conseil que je donnerais à un ingénieur ou à un fondateur Fintech, c’est de prendre le risque et la conformité au sérieux dès le premier jour
Les systèmes financiers reposent sur la confiance. Si vous ne pouvez pas démontrer que vous atténuez les risques, vous perdez cette confiance, et au bout du compte toute l’entreprise
Dire d’éviter les nombres à virgule flottante n’est pas vrai. J’ai travaillé 20 ans dans la Fintech et, la plupart du temps, nous utilisions des double. Excel utilise des double, le front-end utilisera des double, et toutes les bases de données prennent en charge les double. Les bibliothèques standard savent parser des double, et JSON, quelle que soit la théorie, utilise en pratique des double. Beaucoup de systèmes ERP utilisent aussi des double
Le point clé lorsqu’on manipule des devises avec des double est de garder à l’esprit qu’ils peuvent contenir 15 chiffres de précision au total. Tant que les nombres n’utilisent pas plus de chiffres que cela, comme
123456789.01ou123.456789, on peut obtenir une précision décimale parfaite dans les calculs financiers. Il suffit de toujours arrondir le résultat dans la limite de 15 chiffres de précision après chaque calcul et avant chaque comparaison. C’est ce que fait ExcelLe principal avantage des double est qu’ils sont largement pris en charge et qu’ils permettent de mélanger différentes précisions dans un même système. Cela arrive lorsqu’on traite de la finance internationale ou des produits financiers avancés. Certaines comptabilités exigent une précision au millième, d’autres doivent être arrondies par multiples de 0,25. Au final, vous n’utiliserez pas l’arithmétique de base mais une bibliothèque spécialisée de mathématiques comptables, et cette bibliothèque peut parfaitement utiliser la virgule flottante comme backend
La vérification de solde Plaid ne garantit pas qu’un débit ACH que vous allez soumettre réussira
Peu importe que le solde soit d’un million de dollars. Avant que l’ACH soit traité, tout l’argent peut (a) partir par virement bancaire, (b) être compensé par les ACH d’hier — factures, prélèvements automatiques, etc. — et par des chèques, ou (c) être dépensé par carte de débit/au DAB
Il vaut peut-être mieux que je ne dise pas comment je sais que certaines Fintech ne gèrent pas cela
La section sur les clés d’idempotence vaut à elle seule la lecture. La plupart des développeurs apprennent cette leçon à la dure
Beaucoup d’entre eux datent d’avant la diffusion large des connaissances sur l’idempotence, si bien qu’on finit souvent par bricoler une clé d’idempotence en concaténant plusieurs champs censés être globalement uniques. Le problème, c’est qu’ils ne sont jamais totalement uniques. On peut parfois voir ce qui se passe derrière le rideau, par exemple quand une banque empêche d’effectuer le même jour deux virements du même montant vers le même compte bénéficiaire
J’ai passé beaucoup de temps à expliquer comment l’idempotence devrait fonctionner et pourquoi elle est importante. La plupart des équipes comprennent le besoin, mais très peu y avaient pensé dès le départ
La « Fintech » est très vaste, et la majeure partie de ce qu’on appelle Fintech est en réalité de la communication. Communication entre entreprises, entre traders, entre systèmes, entre grands livres. Il n’existe pas de « bonne » façon de programmer valable pour tout le secteur. Au bout du compte, la bonne façon est celle que comprend l’interlocuteur avec qui je communique
Si l’autre partie suit les devises au centime près, les suivre avec une précision supérieure provoquera des écarts d’arrondi. Et inversement, si je travaille au centime alors que l’autre partie travaille au dixième de centime, c’est pareil. Tous les autres conseils de ce document doivent être lus de cette manière