> ## Content Index
> Fetch the complete content index at: https://www.sfeir.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Transactional Outbox : la pièce manquante entre votre base et Kafka
- URL: https://www.sfeir.dev/back/transactional-outbox-la-piece-manquante-entre-votre-base-et-kafka/
- Published: 2026-04-20T13:36:50.000Z
- Updated: 2026-04-20T13:36:50.000Z
- Description: Entre base de données et Kafka, une panne suffit à créer des incohérences. Découvrez le pattern Transactional Outbox avec Spring Boot pour publier des événements fiables, éviter les messages fantômes et sécuriser vos flux en production.
- Author: Erwan Le Tutour
- Tags: Back, Java, Spring Boot, Kafka, Messaging

Dans les deux précédents articles sur Kafka, nous avons vu comment publier et consommer des messages de manière fiable.

[Intégration de Kafka dans une application Spring BootDans un monde où les applications doivent communiquer vite et bien, Kafka joue le rôle de chef d’orchestre silencieux, coordonnant des millions de messages chaque seconde. Voyons comment l’intégrer simplement avec Spring Boot![](https://static.ghost.org/v5.0.0/images/link-icon.svg)sfeir.dev - Le média incontournable pour les passionnés de tech et d'intelligence artificielleErwan Le Tutour![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/size/w1200/2025/08/20250821_1747_Pixel-Messaging-Magic_simple_compose_01k36mx8k8f58szedhng0ct0f0.png)](https://www.sfeir.dev/back/integration-de-kafka-dans-une-application-spring-boot/)

[Intégration de Kafka dans une application Spring Boot - Allons plus loinsAllons plus loins dans l’exploration de Kafka en découvrant comment mettre en place un mécanisme de retry et en garantissant l’unicité des échanges.![](https://static.ghost.org/v5.0.0/images/link-icon.svg)sfeir.dev - Le média incontournable pour les passionnés de tech et d'intelligence artificielleErwan Le Tutour![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/size/w1200/2025/08/20250821_1747_Pixel-Messaging-Magic_simple_compose_01k36mx8k9e3j86xqd2j0fed62.png)](https://www.sfeir.dev/back/integration-de-kafka-dans-une-application-spring-boot-allons-plus-loins/)

Mais en production, une question finit toujours par arriver :

> Comment garantir la cohérence entre une écriture en base et l’envoi d’un événement Kafka ?

Prenons un cas simple : création d’une commande.

- Si la commande est bien enregistrée en base, mais que la publication [Kafka](https://www.sfeir.dev/data/kesaco-apache-kafka/) échoue, l’écosystème ne voit jamais l’événement.
- Si Kafka publie, mais que la transaction base est rollback, les autres services reçoivent un événement fantôme.

Bienvenue dans le vrai monde des systèmes distribués.

C’est précisément le problème que résout le pattern **Transactional Outbox**.

## Qu’est-ce que le Transactional Outbox ?

Le principe est le suivant :

1. On écrit la donnée métier (ex: `order`) dans la base.
2. Dans **la même transaction**, on écrit un enregistrement dans une table `outbox_events`.
3. Un publisher asynchrone lit les événements `PENDING` de l’outbox.
4. Il publie vers Kafka.
5. Il marque ensuite l’événement en `SENT`.

Tant que la transaction SQL n’est pas validée, aucun événement n’est considéré publiable.

## ⚖️ Pourquoi c’est utile ?

### ➕ Avantages

- Garantit la cohérence **DB ↔ événement** sans 2PC.
- Compatible avec les architectures event-driven.
- Permet de rejouer/inspecter les événements non envoyés.
- Se combine très bien avec [une gestion propre des erreurs](https://www.sfeir.dev/back/comment-bien-gerer-ses-erreur-dans-springboot/).

### ➖ Inconvénients

- Ajoute une table et un composant de publication à maintenir.
- Nécessite une stratégie d’observabilité (lag outbox, retries, backlog), voir une [supervision](https://www.sfeir.dev/back/superviser-votre-application-spring-boot/).
- L’idempotence côté consumer reste nécessaire, même avec Outbox.

## Implémentation dans [Spring Boot](https://www.sfeir.dev/back/il-etait-une-fois-spring-boot/)

### Dépendances Maven

Pour commencer, il nous faut ajouter la dépendance suivante à notre fichier `pom.xml`

```xml
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
```

### Le broker Kafka

Pour cet article notre broker Kafka est le même que le précédent, exposé sur le port 9092 depuis un conteneur docker.

### Entité métier + entité Outbox

L’entité métier (`CustomerOrder`) représente la donnée fonctionnelle.

L’entité Outbox stocke l’événement à publier :

```java
@Entity
@Table(name = "outbox_events")
public class OutboxEvent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 80)
    private String aggregateType;

    @Column(nullable = false, length = 64)
    private String aggregateId;

    @Column(nullable = false, length = 120)
    private String eventType;

    @Lob
    @Column(nullable = false)
    private String payload;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private OutboxStatus status;

    @Column(nullable = false)
    private Instant createdAt;

    private Instant sentAt;
}
```

Avec l’état :

```java
public enum OutboxStatus {
    PENDING,
    SENT
}

```

### Écriture atomique: commande + outbox

Le point critique est ici : on persiste commande et outbox dans **une seule transaction**.

```java
@Transactional
public UUID createOrder(CreateOrderRequest request) {
    CustomerOrder order = new CustomerOrder();
    order.setCustomerName(request.customerName());
    order.setAmount(request.amount());
    CustomerOrder savedOrder = customerOrderRepository.save(order);

    OrderCreatedEvent event = new OrderCreatedEvent(
            savedOrder.getId(),
            savedOrder.getCustomerName(),
            savedOrder.getAmount(),
            savedOrder.getCreatedAt()
    );

    OutboxEvent outboxEvent = new OutboxEvent();
    outboxEvent.setAggregateType("ORDER");
    outboxEvent.setAggregateId(savedOrder.getId().toString());
    outboxEvent.setEventType("ORDER_CREATED");
    outboxEvent.setStatus(OutboxStatus.PENDING);
    outboxEvent.setPayload(toJson(event));
    outboxEventRepository.save(outboxEvent);

    return savedOrder.getId();
}

```

Ici, pas de publication Kafka immédiate : uniquement de la persistance fiable.

### Publisher planifié

Un [composant planifié](https://www.sfeir.dev/back/planifier-des-taches-avec-spring-batch/) scanne les événements `PENDING`.

```java
@Scheduled(fixedDelayString = "${app.outbox.publish-delay-ms:3000}")
public void publishPending() {
    outboxEventRepository.findTop50ByStatusOrderByCreatedAtAsc(OutboxStatus.PENDING)
            .forEach(event -> {
                try {
                    outboxPublisherTx.publishOne(event.getId());
                } catch (Exception exception) {
                    LOG.warn("Outbox event {} will be retried later", event.getId(), exception);
                }
            });
}

```

Et la publication atomique sur un événement unique :

```java
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void publishOne(Long outboxEventId) throws Exception {
    OutboxEvent event = outboxEventRepository.findByIdAndStatus(outboxEventId, OutboxStatus.PENDING)
            .orElse(null);

    if (event == null) {
        return;
    }

    kafkaTemplate.send(topic, event.getAggregateId(), event.getPayload())
            .get(10, TimeUnit.SECONDS);

    event.setStatus(OutboxStatus.SENT);
    event.setSentAt(Instant.now());
}

```

### API de démonstration

- `POST /api/orders` : crée une commande et un événement outbox `PENDING`.
- `GET /api/outbox?status=PENDING|SENT` : inspecte la table outbox.

Exemple de requête :

```json
{
  "customerName": "Erwan",
  "amount": 42.50
}

```

## Configuration

```properties
spring.application.name=transactional-outbox-tutorial
server.port=8088

spring.kafka.bootstrap-servers=localhost:9092
app.kafka.topic=order-events
spring.kafka.consumer.group-id=outbox-demo-group

spring.datasource.url=jdbc:h2:mem:outboxdb;DB_CLOSE_DELAY=-1
spring.jpa.hibernate.ddl-auto=update

app.outbox.publish-delay-ms=3000

```

## Tests d’intégration

Deux tests valident le comportement attendu :

1. `OrderServiceIntegrationTest` : vérifie qu’une commande et un événement `PENDING` sont bien persistés ensemble.
2. `OutboxPublisherTxIntegrationTest` : mock la publication Kafka et vérifie la transition `PENDING -> SENT`.

## Bonnes pratiques de prod

- Ajouter un mécanisme de retries/backoff côté publisher (et éventuellement DLQ applicative).
- Exposer des métriques : nombre de `PENDING`, âge max d’un événement, taux d’échec publication.
- Surveiller ces métriques via [Spring Boot Admin](https://www.sfeir.dev/back/gerer-et-superviser-ses-applications-avec-spring-boot-admin/) et [Prometheus et Grafana](https://www.sfeir.dev/back/superviser-votre-application-spring-boot/).
- Garder des tests d’architecture avec [ArchUnit](https://www.sfeir.dev/back/maitrisez-votre-architecture-spring-boot-avec-archunit/) pour éviter les contournements (publication directe hors outbox).

## Conclusion

Le Transactional Outbox ne rend pas un système “magique”.

En revanche, il donne un cadre robuste pour résoudre un problème classique des architectures distribuées :  
**ne jamais laisser la base dire une chose pendant que le bus d’événements en raconte une autre.**

Et si tu veux pousser encore plus loin la robustesse de la chaîne, combine ce pattern avec des [tests de chaos via Chaos Mokey pour Spring Boot](https://www.sfeir.dev/back/introduisez-du-chaos-dans-votre-application-spring-boot/) et une stratégie d’idempotence côté consumer.

---

Tout le code présent dans cet article est trouvable ici :

[GitHub - ErwanLT/springboot-demo: Demo project for spring-boot possibilityDemo project for spring-boot possibility. Contribute to ErwanLT/springboot-demo development by creating an account on GitHub.![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/icon/pinned-octocat-093da3e6fa40-151.svg)GitHubErwanLT![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/thumbnail/1c442eb6-968b-4033-9509-b77aaa738390-50)](https://github.com/ErwanLT/springboot-demo?ref=sfeir.dev)