Plateforme
Plateforme
Conception des classes
Diagrammes de classes UML de l'infrastructure partagée et des dépôts, ainsi que les motifs de conception sur lesquels repose le plan de données.
Sur cette page
Le plan de données sépare les contrats de domaine (internal/domain), l’infrastructure réutilisable (pkg/) et le code des services qui les compose (app/). Les dépendances pointent vers l’intérieur : les dépôts dépendent des interfaces et de l’infrastructure générique, jamais l’inverse.
Infrastructure partagée
L’adaptateur cryptographique réalise l’interface de domaine Packer, de sorte que les services dépendent de l’abstraction et que Tink reste un détail d’implémentation. Le cache et le batcher sont génériques et ne portent aucune connaissance du domaine.
classDiagram
direction LR
class Packer {
<<interface>>
+Pack(plaintext, associatedData) SecureEnvelope
+Unpack(envelope) plaintext
}
class SecureEnvelope {
+Ciphertext bytes
+AttachedData bytes
}
class Crypto {
-primitive tink.AEAD
+Pack(plaintext, associatedData) SecureEnvelope
+Unpack(envelope) plaintext
}
Crypto ..|> Packer : realizes
Packer ..> SecureEnvelope : creates
class Cache~V~ {
-client theine.Cache
-group singleflight.Group
-ttl time.Duration
-staleWindow time.Duration
+GetOrLoad(ctx, key, loader) V
+Set(key, value) void
+Invalidate(key) void
+Close() void
}
class Batcher~K,V~ {
-pending map of K to V
-interval time.Duration
-maxSize int
-flush Flush~V~
+Add(key, value) void
+Close(ctx) void
-flushPending(ctx) void
}
Dépôts
Un diagramme est consacré à chaque forme : le dépôt users écrit directement et scelle les jetons ; le dépôt modules (le dépôt commands est son jumeau) fait passer les écritures de paramètres par le batcher et lui transmet sa méthode flush comme callback. Tous les collaborateurs arrivent par injection au constructeur, ce qui permet aux tests de remplacer le client SQLite en mémoire et le publisher enregistrant les appels.
classDiagram
direction TB
class Publisher {
<<interface>>
+PublishOwned(ctx, subject, payload) error
+PublishOwnedWithID(ctx, subject, id, payload) error
+Flush(ctx) error
+Close() error
}
class UserView {
+ID uint64
+Username string
+DisplayName string
+IsActive bool
+Status string
+Banned bool
+Locale string
+CommandsPageHidden bool
}
class Users {
-client ent.Client
-views Cache of UserView
-packer Packer
-pub Publisher
+Register(ctx, id, username, displayName, email) error
+Get(ctx, id) UserView
+SetStatus(ctx, id, status) error
+Delete(ctx, id) error
+UpsertToken(ctx, userID, type, platform, access, refresh, expiresAt) error
+Token(ctx, userID, type, platform) plaintext
+Reproject(ctx) error
+Invalidate(id) void
}
class ModuleView {
+Name string
+IsEnabled bool
+Configs json
+Revision int
}
class Modules {
-client ent.Client
-views Cache of ModuleView slices
-batcher Batcher
-pub Publisher
+List(ctx, userID) ModuleView slice
+Set(userID, name, enabled, configs) error
+SetNow(ctx, userID, name, enabled, configs) ModuleView slice
+Patch(ctx, userID, name, enabled, partial, expectedRev) PatchResult
+Reproject(ctx) error
+Invalidate(userID) void
-flush(ctx, items) error
}
Users *-- "1" UserView : serves
Users --> Publisher : announces
Modules *-- "1" ModuleView : serves
Modules --> Publisher : announces
Modules ..> Modules : flush is the batcher callback
La composition (losange plein) entre un dépôt et son cache ou son batcher est intentionnelle : le dépôt les crée et les possède, et la fermeture du dépôt les ferme également. Le publisher et le packer sont des associations vers des interfaces possédées ailleurs et injectées à la construction.
Motifs utilisés
| Motif | Emplacement | Raison |
|---|---|---|
| Repository (PoEAA) | app/db/*/repository |
Un objet par agrégat, faisant le lien entre domaine et ent ; c’est le point commun utilisé par chaque test |
| Data Transfer Object | internal/domain/event/data, structures de vue |
Payloads d’état complet sur le bus et vues sans champs sensibles dans les caches |
| Publish/Subscribe (Observer à l’échelle du système) | pkg/bus sur NATS |
Découple les écrivains de chaque lecteur : caches, projecteur et futurs consommateurs |
| Event-Carried State Transfer | Tous les événements de changement | Les consommateurs se mettent à jour à partir du seul événement ; aucun service ne lit le schéma d’un autre |
| Cache read-through avec regroupement des requêtes | pkg/cache |
Singleflight garantit un seul chargeur par clé, quelle que soit la concurrence |
| Write-Behind | pkg/batch |
Regroupe les écritures par clé et effectue une transaction par fenêtre plutôt qu’une écriture par clic |
| CQRS, modèle de lecture | app/projector et Valkey |
Le côté écriture reste normalisé dans MySQL ; le côté lecture est une projection dénormalisée |
| Adaptateur | pkg/crypto (Tink derrière Packer), pkg/bus (nats.go derrière les contrats Publisher, Subscriber et Message) |
Les API tierces restent derrière des interfaces possédées par l’application |
| Injection de dépendances | Chaque constructeur NewX |
La composition a lieu dans main, et les tests injectent des doublures |
| Récepteur idempotent | Chaque consommateur, Transactions.Record |
La livraison au moins une fois et les nouvelles tentatives de webhook ne doivent pas appliquer deux fois une opération |
Motifs absents
Les motifs évités le sont tout aussi délibérément. Il n’existe ni service locator ni registre global : tout arrive par le constructeur. Il n’y a pas de noyau partagé d’entités entre les services : chacun possède son schéma ent, et les seuls types partagés sont les DTO d’événements. Il n’y a pas non plus d’abstraction prématurée de la base de données : les dépôts parlent directement à ent, car le remplacement de MySQL est déjà couvert au niveau du pilote et du dialecte (ADR 0005).