Producing Events with Spring Kafka

The Event and the Topic

Add spring-kafka (Spring for Apache Kafka; Spring Boot manages the version). Define the event as a record. Put an eventId in every event so consumers can detect duplicates:

public record OrderPlacedEvent(
        String eventId,            // UUID, for idempotent consumers
        Long orderId,
        Long userId,
        BigDecimal totalAmount,
        List<Item> items,
        Instant occurredAt) {

    public record Item(Long productId, int quantity) {}
}

@Configuration
class KafkaTopicConfig {

    @Bean
    NewTopic orderPlacedTopic() {          // created at startup if missing
        return TopicBuilder.name("order-placed")
                .partitions(3)
                .replicas(1)               // 3 in production
                .build();
    }
}

Sending with KafkaTemplate

@Service
@RequiredArgsConstructor
@Slf4j
public class OrderEventPublisher {

    private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate;

    public void orderPlaced(Order order) {
        OrderPlacedEvent event = new OrderPlacedEvent(
                UUID.randomUUID().toString(), order.getId(), order.getUserId(), order.getTotal(),
                order.getItems().stream().map(i -> new OrderPlacedEvent.Item(i.getProductId(), i.getQuantity())).toList(),
                Instant.now());

        // key = orderId: all events of one order stay in order on one partition
        kafkaTemplate.send("order-placed", String.valueOf(order.getId()), event)
                .whenComplete((result, ex) -> {
                    if (ex != null) {
                        log.error("Could not publish OrderPlaced {}", order.getId(), ex);
                    } else {
                        log.info("OrderPlaced {} -> partition {} offset {}", order.getId(),
                                result.getRecordMetadata().partition(), result.getRecordMetadata().offset());
                    }
                });
    }
}

order-service application.yml

application.yml

spring:
  kafka:
    bootstrap-servers: ${KAFKA_SERVERS:localhost:9092}
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      acks: all                       # wait until all in-sync replicas have it
      properties:
        enable.idempotence: true      # no duplicates from producer retries
        spring.json.add.type.headers: false

Example output

INFO  c.s.order.OrderEventPublisher : OrderPlaced 1001 -> partition 2 offset 17

$ docker exec kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
    --topic order-placed --from-beginning --property print.key=true
1001	{"eventId":"6b1f0c2e-8d3a-4f7b-9e21-5c4d3a2b1f0e","orderId":1001,"userId":42,"totalAmount":2998.00,
      "items":[{"productId":7,"quantity":2}],"occurredAt":"2026-10-14T10:15:30.123Z"}

Note: send() is asynchronous: it returns a CompletableFuture<SendResult>. If you save the order in the database and then publish, a crash in between loses the event. The outbox pattern (advance level) fixes that.