java 130 lines · 4 tabs

Publishing Domain Events After Commit with @TransactionalEventListener in Spring Boot

Shared by codesnips Aug 2026
4 tabs
package com.example.orders.event;

import java.math.BigDecimal;
import java.time.Instant;

public final class OrderPlacedEvent {

    private final Long orderId;
    private final BigDecimal total;
    private final Instant occurredAt;

    public OrderPlacedEvent(Long orderId, BigDecimal total) {
        this.orderId = orderId;
        this.total = total;
        this.occurredAt = Instant.now();
    }

    public Long getOrderId() {
        return orderId;
    }

    public BigDecimal getTotal() {
        return total;
    }

    public Instant getOccurredAt() {
        return occurredAt;
    }
}
4 files · java Explain with highlit

This snippet shows how a Spring Boot service publishes domain events that fire only after the surrounding database transaction has actually committed, avoiding the classic bug where a downstream side effect (an email, a Kafka message, a cache eviction) runs but the transaction later rolls back, leaving consumers acting on state that never persisted.

In OrderPlacedEvent, the event is a plain immutable value object carrying just enough context — the orderId and total — for listeners to react without reaching back into the database. Events are intentionally thin so they can be serialized or forwarded later.

In OrderService, the business logic runs inside a @Transactional method. The order is saved through the JPA repository, and then ApplicationEventPublisher.publishEvent is called. This is the subtle part: publishing an event inside a transaction does not immediately notify @TransactionalEventListener beans. Instead Spring registers the event with the current transaction's synchronization and holds it until a specific phase. Because publication happens while the transaction is still open, the listener sees a consistent view and is guaranteed the commit succeeded before it runs.

In OrderEventListener, the handler is annotated with @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT), so it executes only once the commit is durable. The @Async annotation moves the work off the committing thread so slow I/O like sending to Kafka does not extend the transaction or block the caller. A key pitfall is that AFTERCOMMIT listeners run outside the original transaction, so any database writes they perform need a new transaction — hence `@Transactional(propagation = Propagation.REQUIRESNEW)on the audit write. Another trap: if no transaction is active when the event is published, an AFTER_COMMIT listener is silently skipped unlessfallbackExecution` is enabled.

The trade-off is that AFTER_COMMIT work is best-effort — if the JVM dies between commit and delivery, the event is lost. When at-least-once delivery matters, this pattern is usually paired with a transactional outbox table written in the same transaction. For eviction, notification, and analytics, though, @TransactionalEventListener is the clean, idiomatic Spring choice. AsyncConfig wires the executor that backs @Async so failures surface on a dedicated pool.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
go
package dbutil

import (
  "context"

  "github.com/jackc/pgconn"

Retry Postgres serialization failures with bounded attempts

go postgres transactions
by Leah Thompson 1 tab
graphql
type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
    createdAt: String!

GraphQL API with Spring Boot

java graphql spring-boot
by David Kumar 3 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
java
package com.example.starter.config;

import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;

Custom Spring Boot starters

java spring-boot starter
by David Kumar 4 tabs
ruby
class CreateDeadJobs < ActiveRecord::Migration[7.1]
  def change
    create_table :dead_jobs do |t|
      t.string  :jid, null: false
      t.string  :queue, null: false
      t.string  :klass, null: false

Background Job Dead Letter Queue (DLQ) Table

rails reliability background-jobs
by codesnips 4 tabs

Share this code

Here's the card — post it anywhere.

Publishing Domain Events After Commit with @TransactionalEventListener in Spring Boot — share card
Link copied