Course topics

By WebNest Studio

Spring Boot Tutorial

Spring Application Events

When a student completes a course, several things should happen: issue a certificate, send a congratulations email, update the leaderboard, notify the instructor. If CourseService calls all of those services directly, it becomes coupled to every feature that cares about course completion, and each new feature means editing it again.

Application events decouple this. The service publishes a CourseCompletedEvent, and any number of listeners react independently. This lesson covers publishing events, synchronous and asynchronous listeners, ordering and conditions, transaction-bound listeners, and when to move from in-process events to a message broker or Spring Modulith's event publication registry.

Publishing Events

Any object can be an event — a Java record is ideal. Inject ApplicationEventPublisher and call publishEvent(new CourseCompletedEvent(...)). Name events in the past tense (something that has happened) and include the data listeners typically need, such as ids and key values, rather than whole mutable entities.

Listening with @EventListener

Annotate a bean method with @EventListener; its parameter type decides which events it receives. By default listeners run synchronously on the publisher's thread and inside its transaction, in an order you can control with @Order. An exception thrown by a synchronous listener propagates back to the publisher. A condition attribute with SpEL filters events, and a listener method can return a new event to publish a follow-up.

Asynchronous Listeners

Add @Async (with @EnableAsync) to run a listener on a background thread, so slow reactions such as sending email do not delay the user's request and failures do not affect the publisher.

Transaction-Bound Listeners

@TransactionalEventListener delays the listener until the publisher's transaction reaches a phase — AFTER_COMMIT by default, or AFTER_ROLLBACK, AFTER_COMPLETION, BEFORE_COMMIT. This guarantees you never email a certificate for a completion that was rolled back. If the listener itself needs to write to the database, give it its own transaction with @Transactional(propagation = REQUIRES_NEW).

Limits and Next Steps

In-process events vanish if the application crashes between commit and listener execution. When reliability matters, use Spring Modulith's event publication registry, which stores events in the database and retries incomplete ones, or publish to an external broker using the transactional outbox pattern. Events that other services must receive belong in Kafka or RabbitMQ.

Examples

Publishing an event and reacting in independent listeners

Java
public record CourseCompletedEvent(Long studentId, String studentEmail, String courseSlug, Instant completedAt) {}

@Service
public class ProgressService {

    private final EnrollmentRepository enrollments;
    private final ApplicationEventPublisher events;

    public ProgressService(EnrollmentRepository enrollments, ApplicationEventPublisher events) {
        this.enrollments = enrollments;
        this.events = events;
    }

    @Transactional
    public void completeLesson(Long enrollmentId, Long lessonId) {
        Enrollment e = enrollments.findById(enrollmentId).orElseThrow();
        e.markLessonDone(lessonId);
        if (e.isCourseComplete()) {
            events.publishEvent(new CourseCompletedEvent(e.getStudentId(), e.getStudentEmail(),
                e.getCourseSlug(), Instant.now()));
        }
    }
}

@Component
class CertificateListener {
    @TransactionalEventListener                      // AFTER_COMMIT
    void issue(CourseCompletedEvent e) {
        System.out.println("Certificate issued for " + e.courseSlug() + " to student " + e.studentId());
    }
}

@Component
class CongratulationsEmailListener {
    @Async
    @TransactionalEventListener
    void email(CourseCompletedEvent e) {
        System.out.println("Emailing " + e.studentEmail() + " on " + Thread.currentThread().getName());
    }
}

@Component
class LeaderboardListener {
    @EventListener(condition = "#e.courseSlug() == 'spring-boot'")
    void bonus(CourseCompletedEvent e) {
        System.out.println("+500 points for completing Spring Boot");
    }
}
Output
+500 points for completing Spring Boot                 (synchronous, inside the transaction)
-- transaction commits --
Certificate issued for spring-boot to student 7
Emailing asha@webnest.in on task-2                      (async, after commit)

Chaining events and listener ordering

Java
public record CertificateIssuedEvent(Long studentId, String certificateNo) {}

@Component
class CertificateListener {

    // Returning an object publishes it as a new event
    @EventListener
    @Order(1)
    CertificateIssuedEvent issue(CourseCompletedEvent e) {
        String no = "WN-" + e.studentId() + "-" + e.courseSlug().toUpperCase();
        return new CertificateIssuedEvent(e.studentId(), no);
    }
}

@Component
class LinkedInShareListener {
    @EventListener
    void suggestShare(CertificateIssuedEvent e) {
        System.out.println("Suggest sharing certificate " + e.certificateNo());
    }
}
Output
Suggest sharing certificate WN-7-SPRING-BOOT

Testing that an event is published

Java
@SpringBootTest
@RecordApplicationEvents
class ProgressServiceTest {

    @Autowired ProgressService progress;
    @Autowired ApplicationEvents events;

    @Test
    void publishesCompletionWhenLastLessonIsDone() {
        progress.completeLesson(1L, 60L);

        assertThat(events.stream(CourseCompletedEvent.class))
            .singleElement()
            .extracting(CourseCompletedEvent::courseSlug)
            .isEqualTo("spring-boot");
    }
}
Output
ProgressServiceTest > publishesCompletionWhenLastLessonIsDone() PASSED

Common Mistakes

  • Sending emails or calling external APIs from plain @EventListener methods inside a transaction that may still roll back.
  • Expecting @TransactionalEventListener to fire when no transaction is active (it is skipped unless fallbackExecution = true).
  • Publishing JPA entities as events, then reading lazy associations in async listeners after the session is closed.
  • Letting a failing synchronous listener break the main business operation unintentionally.
  • Using in-process events for cross-service communication; they never leave the JVM.

Key Points to Remember

  • ApplicationEventPublisher publishes events; @EventListener methods receive them by type.
  • Listeners run synchronously by default; add @Async for background execution.
  • @TransactionalEventListener runs after commit (or another phase) to avoid acting on rolled-back data.
  • Use conditions, @Order and returned events for filtering, ordering and chaining.
  • For guaranteed delivery use Spring Modulith's event registry or a message broker.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.