krizaka-platform-kit
Platform kit
Le code transverse que chaque service Spring Boot écrit — écrit une fois, avec ses invariants testés.
Le problème qu'elle supprime
Un événement publié après le commit se perd au prochain crash.
Publier un événement juste après l'écriture en base : un crash entre les deux et l'événement est perdu ; l'envoyer avant, et un rollback a déjà prévenu tout le monde. Un @RabbitListener en échec remis en file à l'infini bloque sa file. Chaque service invente son JSON d'erreur. Dans les services d'Orazaka, ce code existait cinq à sept fois — avec trois bugs actifs entre les copies.
- Double écriture : un service qui écrit sa ligne puis appelle
rabbitTemplate.convertAndSendperd l'événement s'il plante entre les deux, et celui qui envoie d'abord annonce des changements annulés. - Messages empoisonnés : un listener qui lève une exception sur un message malformé le reçoit de nouveau, aussitôt, pour toujours, et plus rien ne passe dans la file.
- Déduplication à la main : un « existe-t-il ? alors j'insère » laisse passer deux livraisons qui se chevauchent — exactement quand le broker relivre une première tentative lente — et une réservation gardée après un échec perd la relivraison en silence.
- Chaînes de sécurité copiées par service : deux sur six avaient dérivé dans Orazaka ; l'une répondait 401 à un preflight CORS.
Ce qu'elle fait
- Les événements passent par un outbox transactionnel : validés ou annulés avec vos données.
- Les files réessaient 5 fois puis passent en
.dlq— un message empoisonné ne boucle jamais. - Une base de sécurité et des erreurs RFC 9457 avec un
requestId, dans chaque service.
Quatre modules et quatre starters : une base de sécurité pour chaque chaîne de filtres, des événements par outbox transactionnel, des files consommatrices qui réessaient puis passent en dead-letter, une consommation idempotente, des erreurs RFC 9457 avec un identifiant de requête, et des noms de service obligatoires sur chaque métrique.
Rien n'est ouvert par oubli.
SecurityBaselinetermine chaque chaîne paranyRequest().authenticated(): le chemin que vous avez oublié de lister est fermé, pas public.
Décisions et compromis
Nous avons choisi
Un outbox transactionnel :
EventPublisher.publishécrit une ligne de votre outbox dans votre transaction ; un relais réserve les lignes avecFOR UPDATE SKIP LOCKEDet les publie, avec unmessageIdchoisi à l'écriture.Nous avons refusé
Publier vers le broker depuis la méthode métier, et une transaction distribuée (XA) entre base et broker.
Parce que
L'événement est validé ou annulé avec le changement. Un relais qui plante republie le même
messageId, et la déduplication du consommateur en fait un non-événement.Ce que cela vous coûte
Une table d'outbox par contexte (vous implémentez
OutboxStoresur votre schéma) et un délai de scrutation entre le commit et la publication.Nous avons choisi
L'enveloppe dans les en-têtes AMQP (
kz-type,kz-version,kz-producer,kz-correlation-id,kz-occurred-at), le corps = l'événement nu, et un JSON Schema par événement dans le module-apidu producteur.Nous avons refusé
Un jar partagé de DTO d'événements, et une enveloppe JSON autour de chaque corps.
Parce que
Un jar de DTO partagé lie la version de chaque service à celle de tous les autres. Un schéma vérifié par des tests des deux côtés (
EventContractTest) donne le contrat sans le couplage, et le corps reste lisible dans n'importe quel langage.Ce que cela vous coûte
Chaque consommateur garde sa propre copie tolérante de l'événement et un test de contrat.
Nous avons choisi
Des files quorum déclarées en une ligne (
KrizakaQueues.consumer), une relance sans état avec back-off — 5 tentatives à partir de 500 ms — puis sa jumelle.dlqavec les en-têtes d'origine et l'exception.Nous avons refusé
La remise en file par défaut de Spring en cas d'échec.
Parce que
Un message empoisonné remis en file tourne en boucle ; dans la DLQ il est visible, avec sa raison, et se rejoue à la main.
Ce que cela vous coûte
Une file déjà déclarée « classic » doit être vidée et redéclarée une fois.
Nous avons choisi
RFC 9457 Problem Details pour toute erreur : une
DomainExceptiondevient son 4xx avec un code stable et lerequestId; la validation un 422 avecerrors[]; tout l'inattendu un 500 qui ne porte jamais son message.Nous avons refusé
Des corps d'erreur libres par service, et des traces ou messages d'exception dans les réponses 500.
Parce que
Un client gère une seule forme, et le
requestIdde la réponse est celui de la ligne de log — un ticket de support pointe l'échec.Ce que cela vous coûte
Vos exceptions étendent
DomainException(NotFoundException,ConflictException…) pour choisir leur statut.Nous avons choisi
Des starters qui ne sont que des POM, au-dessus de modules utilisables seuls ; des valeurs par défaut à la plus basse priorité.
Nous avons refusé
Renommer les modules en starters, et des défauts qui écrasent
application.yml.Parce que
Un produit déclare des capacités, pas la liste Spring derrière ; une montée de Spring Boot change les starters, pas votre POM — et votre
application.ymlgagne toujours.Ce que cela vous coûte
Java 21 et Spring Boot 4.0 uniquement.
En code
@Transactional
public User register(NewUser input) {
User user = users.save(input);
// A row of YOUR outbox, in THIS transaction: no event for a rollback, no lost event on a crash.
events.publish("evt.user.registered", 1, new UserRegistered(user.id(), user.email()));
return user;
}
@Bean // the quorum queue, its <queue>.dlq, the bindings
Declarables userEvents(MessagingExchanges exchanges) {
return KrizakaQueues.consumer(exchanges, "krizaka.notifications.user-events", "evt.user.registered");
}
@RabbitListener(queues = "krizaka.notifications.user-events") // 5 retries with back-off, then the DLQ
void onRegistered(UserRegistered event, @Header(AmqpHeaders.MESSAGE_ID) String messageId) {
if (!dedup.claim("notifications.user-events", messageId)) return; // a redelivery: already done
try { welcome(event); }
catch (RuntimeException e) { dedup.release("notifications.user-events", messageId); throw e; }
}Ne l'utilisez pas quand
- Vos services tournent sur Spring Boot 3 ou Java 17 — le kit cible Spring Boot 4.0 et Java 21.
- Votre couche HTTP est WebFlux :
krizaka-webne configure que les applications servlet. - Votre bus est Kafka :
krizaka-messagingest RabbitMQ (AMQP 0-9-1) seulement. - Vos jetons viennent d'un fournisseur d'identité externe qui signe en RS256 ou ES256 :
krizaka-securityvérifie du HS256 aujourd'hui.
Où elle en est
0.2.0 sur Maven Central (2026-10-10) : les quatre modules et les quatre starters, construits et testés sur un vrai PostgreSQL et un vrai RabbitMQ en CI.
Publié
com.krizaka:krizaka-spring-boot-starter-* 0.2.0 · Maven Centralcom.krizaka:krizaka-web · -security · -messaging · -observability 0.2.0 · Maven CentralEn cours
MessageDedup.processOnce— réserver, exécuter, rendre la réservation en cas d'échec. Cinq listeners de notifications et de billing répètent ces huit lignes aujourd'hui ; l'un d'eux décrit l'oubli du release comme « le règlement perdu pour toujours ». Arrive en 0.3.0.krizaka-platform-kit#12- Un seul secret HS256 signe et vérifie : un service qui peut vérifier peut émettre. Décidé : des jetons de session signés par leur émetteur (JWKS) et des jetons de service par appelant, HS256 déprécié sur une mineure.krizaka-platform-kit#11
L'adopter
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.krizaka</groupId>
<artifactId>krizaka-bom</artifactId>
<version>0.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency><groupId>com.krizaka</groupId><artifactId>krizaka-spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>com.krizaka</groupId><artifactId>krizaka-spring-boot-starter-security</artifactId></dependency>
<dependency><groupId>com.krizaka</groupId><artifactId>krizaka-spring-boot-starter-rabbitmq</artifactId></dependency>
<dependency><groupId>com.krizaka</groupId><artifactId>krizaka-spring-boot-starter-observability</artifactId></dependency>
</dependencies>Dites-nous où ça coince.
Une brique est juste quand elle survit à votre code, pas au nôtre. Posez votre question dans le fil de la brique, proposez un changement comme idée, ou signalez un bug sur son dépôt — chaque décision de cette page reste ouverte à un meilleur argument.
Les autres briques
- UtilisateursL'inscription ressemble à un week-end. Jetons de reset, OAuth et JWT en font un trimestre.
- NotificationsUn e-mail envoyé dans une transaction annulée ne se rattrape pas.
- Facturation & créditsDébiter après, et le travail tourne à crédit. Débiter avant, et les échecs sont facturés.
- Build, BOM & kit de testLes POM parents dérivent — et un BOM Spring écrase en silence la version de Boot choisie.
- Krizaka UITrois produits, trois vocabulaires de jetons, 1 153 surcharges
light:dans une seule app.