BUILDING TO SCALE
FR | EN
Parler de votre projet

Concevoir une architecture de services Rails autour de SEPA et des flux de paiement

Les paiements changent les questions de conception auxquelles une codebase Rails doit répondre. Notes sur l'isolation des mouvements d'argent, les ledgers, et la modélisation de SEPA comme le processus asynchrone qu'il est.

Publié le 8 février 2026 · 5 min de lecture

Les paiements sont la seule partie d'une codebase Rails où « il suffit d'ajouter un modèle » ne fonctionne plus. Dès que de l'argent réel circule, les questions de conception changent : que se passe-t-il si le webhook de la banque n'arrive jamais, que se passe-t-il s'il arrive deux fois, et qui a le droit de lire un solde.

Isoler les mouvements d'argent du reste de l'application

Nous extrayons systématiquement la logique de paiement dans son propre service interne, même au sein d'un monolithe, avec ses propres modèles et ses propres tables clairement préfixées par un namespace. Le reste de l'application lui parle à travers une interface réduite et explicite : initier un virement, obtenir le statut courant. Rien en dehors de cette frontière n'écrit directement sur un solde.

APP RAILS SERVICE DE PAIEMENT LEDGER (AJOUT SEUL) PARTENAIRE BANCAIRE (RÉSEAU SEPA) WEBHOOK MACHINE À ÉTATS
Isoler les mouvements d'argent du reste de l'application

Un ledger en ajout seul, pas une colonne de solde modifiable

Une colonne de solde incrémentée et décrémentée sur place finira par diverger de la réalité, généralement découverte lors d'un rapprochement, généralement au pire moment. Nous utilisons à la place un ledger en ajout seul composé d'entrées individuelles, et nous calculons le solde en les additionnant. Cela coûte une requête ; cela offre une piste d'audit complète et rejouable pour chaque euro qui a bougé.

class LedgerEntry < ApplicationRecord
  belongs_to :account
  validates :amount_cents, presence: true
  # les entrées sont créées, jamais modifiées ni supprimées
end

def balance_cents(account)
  account.ledger_entries.sum(:amount_cents)
end

SEPA est asynchrone par nature, modélisez-le ainsi

Un virement SEPA n'est pas un simple appel d'API avec une réponse immédiate oui ou non. Il traverse des états sur plusieurs heures : soumis, accepté par la banque, réglé, ou rejeté plusieurs jours plus tard pour des raisons indépendantes de votre volonté. Nous le modélisons explicitement comme une machine à états avec un petit ensemble fixe de transitions, pilotée par des webhooks, et nous rendons chaque transition idempotente, car les partenaires bancaires finiront, tôt ou tard, par envoyer deux fois le même webhook.

Le rapprochement est une fonctionnalité, pas une réflexion tardive

Toutes les architectures de paiement que nous avons construites incluent un job planifié qui compare le ledger interne au relevé du partenaire bancaire et lève une alerte sur le moindre écart, aussi petit soit-il. La première fois qu'il détecte quelque chose, c'est généralement dans le premier mois, et c'est généralement une race condition que personne n'avait anticipée lors de la conception.