Krizaka
Toutes les briques

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.convertAndSend perd 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. SecurityBaseline termine chaque chaîne par anyRequest().authenticated() : le chemin que vous avez oublié de lister est fermé, pas public.

La règle du faucon

Décisions et compromis

  1. Nous avons choisi

    Un outbox transactionnel : EventPublisher.publish écrit une ligne de votre outbox dans votre transaction ; un relais réserve les lignes avec FOR UPDATE SKIP LOCKED et les publie, avec un messageId choisi à 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 OutboxStore sur votre schéma) et un délai de scrutation entre le commit et la publication.

  2. 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 -api du 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.

  3. 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 .dlq avec 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.

  4. Nous avons choisi

    RFC 9457 Problem Details pour toute erreur : une DomainException devient son 4xx avec un code stable et le requestId ; la validation un 422 avec errors[] ; 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 requestId de 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.

  5. 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.yml gagne toujours.

    Ce que cela vous coûte

    Java 21 et Spring Boot 4.0 uniquement.

En code

javaUserService.java · UserEventsListener.java
@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-web ne configure que les applications servlet.
  • Votre bus est Kafka : krizaka-messaging est RabbitMQ (AMQP 0-9-1) seulement.
  • Vos jetons viennent d'un fournisseur d'identité externe qui signe en RS256 ou ES256 : krizaka-security vé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é

En 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

xmlpom.xml
<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