Skip to content

Simplified Learning Blog

Learning made easy

  • Home
  • Java
    • Core Java Tutorial
    • Java 8
    • What is Rest API in java
    • Spring Framework
    • Type Casting in Java | 2 types Implicit and explicit casting
    • JUnit 5 Tutorial
      • Assertall in JUnit 5
      • Assertions in JUnit 5
  • Java Interview Questions
    • Top 50 Core Java Interview Questions & Answers (2026 Edition)
    • Top 20 Spring Boot Interview Questions for Freshers (2026 Edition): The Ultimate Cheat Sheet
    • Top 40+ Multithreading interview questions
    • Top 10 AWS Lambda interview questions
  • Java Thread Tutorials
    • How to create thread in Java
    • Multithreading in java
    • Daemon Thread in Java | How to create daemon thread in java
    • Java Virtual Threads (Project Loom) in Real Enterprise Applications
    • WebFlux vs. Virtual Threads in 2026: The Senior Architect’s Decision Matrix
  • AWS
    • What is AWS (Amazon Web Services)
    • AWS IAM (Identity and Access Management)
    • AWS SNS | What is SNS
    • What is SQS | AWS SQS (Simple Queue Service)
    • What is AWS Lambda
  • Software Architecture
    • Software Architecture Performance
    • Performance Principles of Software Architecture
    • Practical System Design Examples using Spring Boot, Queues, and Caches (2026 Guide)
    • System Performance Objective
  • Spring Boot Tutorial
    • Spring Boot Rest API Example complete guide
    • Spring MVC vs. Spring Boot in 2026: The Senior Architect’s Definitive Guide
    • Spring Boot Application.properties vs. YAML: The 2026 Architect’s Verdict
  • Core Java Deep Dives
    • Java int to String Conversion: Performance Benchmarks & Memory Pitfalls
    • String to Integer Conversion in Java | Java convert string to int
    • Converting PDF to JSON in Java Top 3 ways to code:
    • Calculate date of birth from age in jquery
    • How to convert excel to PDF using java
    • jcalendar in java swing example
    • Series program in java
  • Tools
    • JSON Formatter & Debugging Guide for Spring Boot Developers
    • Free Character Counter Tool: The Ultimate Guide to Counting Characters, Words, and Text Statistics
  • Tech Blogs
    • Java 21 New Features
    • Is Java Dead? Is java dead, 2023 ?
    • New Features in Java 17
  • Toggle search form

Testing Asynchronous Flows with Awaitility: The End of Flaky Tests

Posted on January 19, 2026January 19, 2026 By Govind No Comments on Testing Asynchronous Flows with Awaitility: The End of Flaky Tests

TL;DR: The Executive Summary

If you only have 30 seconds, here is why your CI pipeline is red:

Table of Contents

Toggle
    • TL;DR: The Executive Summary
  • The “Thread.sleep” Anti-Pattern explained
  • Phase 1: Getting Started with Awaitility
    • Dependency Setup
    • Refactoring to Robustness
  • Phase 2: Advanced Patterns for Heavy Lifting
    • 1. Handling “Not Ready Yet” Exceptions
    • 2. Testing Kafka & Message Queues
  • Phase 3: Debugging the Un-Debuggable
  • Phase 4: Writing Custom Matchers
  • Phase 5: The Kotlin DSL (Bonus)
  • Common Pitfalls to Avoid
  • Frequently Asked Questions
  • Conclusion
  1. The Villain: Thread.sleep(5000). It makes tests slow (wasted time) and flaky (fails if the server takes 5.1s).
  2. The Hero: Awaitility. It polls your code: “Are we done? No. Are we done? No. Are we done? Yes!”
  3. The Strategy: Use it for specific state checks (DB rows, Kafka messages, Atomic variables).
  4. The Quick Fix: Replace simple sleeps with:
    await().atMost(5, SECONDS).until(() -> repo.count() == 1);

We’ve all been there. You write a pristine integration test for your user registration flow. It sends a message to Kafka, consumes it, and creates a wallet entry in PostgreSQL.

It passes on your high-performance MacBook M3. You push to production with confidence.

Boom. The Jenkins build fails. You re-run it. It passes. You whisper “Heisenbug” to yourself and move on. Two days later, it fails again, stalling the entire release train. Your team starts ignoring the red builds, and code quality plummets.

The culprit is almost always `Thread.sleep()`. Hardcoded waits assume your CI server is as fast as your laptop. It isn’t. In 2026, with widespread event-driven microservices, we need a smarter way to assert “eventual consistency.” Enter Awaitility.

The “Thread.sleep” Anti-Pattern explained

Let’s look at the crime scene. This is a typical “Junior Developer” approach to testing an asynchronous Kafka consumer:

// ❌ THE BUGGY WAY
userService.register(user);
// "Let's give Kafka 2 seconds to process"
// CRIME: This assumes network latency fits in 2s.
Thread.sleep(2000); 
// Sometimes this fails if Kafka takes 2.1 seconds!
// Sometimes successful after 0.1s, wasting 1.9s of build time!
assertNotNull(walletRepository.findByUser(user));

Why is this catastrophic for engineering teams?

  • It fails randomly: On a busy AWS t3.micro instance during peak load, 2 seconds might not be enough.
  • It slows down deployment: If the task finishes in 50ms, you are still waiting 1950ms. Multiply that by 500 integration tests, and you’ve explicitly added 15 minutes of idle time to your build cost.

Phase 1: Getting Started with Awaitility

Awaitility is a DSL (Domain Specific Language) that turns “wait” into a descriptive assertion. It polls your code, asking “Are we there yet?” every few milliseconds.

Dependency Setup

If you are using Spring Boot Starter Test, verify this is included. If not, add it explicitly:

<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <scope>test</scope>
</dependency>

Refactoring to Robustness

Let’s rewrite that test properly. Using `untilAsserted` is my preferred method because it lets you use your standard JUnit/AssertJ assertions inside the block.

// âś… THE SENIOR ARCHITECT WAY
userService.register(user);
await()
    .atMost(Duration.ofSeconds(5)) // Maximum limit
    .pollInterval(Duration.ofMillis(100)) // Check frequency
    .untilAsserted(() -> {
        // This block runs repeatedly until it passes or times out
        Wallet wallet = walletRepository.findByUser(user);
        
        // Use standard assertions!
        assertThat(wallet).isNotNull();
        assertThat(wallet.getBalance()).isEqualTo(100);
    });

Why this is faster: If Kafka processes the message in 40ms, Awaitility detects it on the first poll and the test finishes in ~45ms. No wasted time. This ensures your tests run at the speed of your code, not the speed of your hardcoded sleeps.

Phase 2: Advanced Patterns for Heavy Lifting

Basic polling is easy. But production systems are messy. Here is how I use Awaitility in large-scale microservices handling millions of events.

1. Handling “Not Ready Yet” Exceptions

When testing APIs or databases, calling `findById` too early often throws an `EmptyResultDataAccessException` or `EntityNotFoundException` instead of just returning null. By default, Awaitility crashes the test on the first exception.

The Fix: Use `ignoreExceptions()`.

await()
    .ignoreException(EntityNotFoundException.class) // Keep trying if this happens
    .atMost(10, TimeUnit.SECONDS)
    .until(() -> {
        // If this throws EntityNotFoundException, Awaitility just waits and tries again
        return userStatusService.getStatus(userId).equals("ACTIVE");
    });

2. Testing Kafka & Message Queues

Testing void methods that consume messages is tricky because there is no return value to check. We often use an `AtomicReference` or a concurrent list to capture the side effect.

// Capture the message in a thread-safe container
AtomicReference<String> capturedMessage = new AtomicReference<>();
// Mock the listener to update our container
doAnswer(invocation -> {
    capturedMessage.set(invocation.getArgument(0));
    return null;
}).when(messageListener).onMessage(anyString());
// Trigger the flow
producer.send("Hello World");
// Wait until the container has the expected value
await().untilAtomic(capturedMessage, equalTo("Hello World"));

Phase 3: Debugging the Un-Debuggable

The worst part of async tests is when they fail with: ConditionTimeoutException: Condition not satisfied within 10 seconds.

Great. But why? Was the value null? Was it “PENDING” instead of “COMPLETED”?

I configure Awaitility globally in my `BaseIntegrationTest` to be more verbose. This is a game changer for debugging failures.

@BeforeAll
static void setup() {
    Awaitility.setDefaultPollInterval(100, TimeUnit.MILLISECONDS);
    Awaitility.setDefaultTimeout(5, TimeUnit.SECONDS);
    // CRITICAL: This ensures intermediate exceptions are logged!
    Awaitility.catchUncaughtExceptions(); 
}

Alias your waits: When you have multiple awaits in one test, generic timeout errors are confusing. Give them a name:
await("Email Service should send welcome email").until(...). Now your failure logs will scream exactly which business logic step failed.

Phase 4: Writing Custom Matchers

Sometimes your condition is complex. Instead of writing a massive lambda, write a custom Hamcrest matcher. This makes your tests read like English.

await().until(inventoryService::getStock, hasEnoughItemsFor(order));
// Custom Matcher
private Matcher<Integer> hasEnoughItemsFor(Order order) {
    return new TypeSafeMatcher<Integer>() {
        @Override
        protected boolean matchesSafely(Integer stock) {
            return stock >= order.getQuantity();
        }
        @Override
        public void describeTo(Description description) {
            description.appendText("stock should be greater than " + order.getQuantity());
        }
    };
}

Phase 5: The Kotlin DSL (Bonus)

If you are lucky enough to be using Kotlin in 2026, the syntax gets even improved. Awaitility provides a Kotlin extension module that removes the Java boilerplate.

// Kotlin Syntax - Clean and Readable
await untilCallTo { userService.count() } matches { it == 5 }
// Or with lambda
await until { 
    walletRepository.findAll().any { it.balance > 0 } 
}

Common Pitfalls to Avoid

1. While Loop Checks: Do not write your own `while(true) { Thread.sleep(100) }` loop. You will implement it poorly. Awaitility handles thread interruption, timeouts, and exception damping correctly. Don’t reinvent the wheel.

2. External Calls: Never await on a real HTTP call to Stripe or PayPal. You will get rate-limited or banned. ALWAYS specific mock external services (using WireMock) and await on the internal state change (DB update) instead.

Frequently Asked Questions

Q: Is it better than `StepVerifier` in Spring WebFlux?

They solve different problems. `StepVerifier` is for testing the Reactive Stream flow itself (Next, Next, Complete) synchronously within the test context. Awaitility is for testing the asynchronous side effects of that flow (e.g., did the database actually update after the stream completed?). In valid integration tests, I often use both together.

Q: Does polling cause CPU spikes?

Yes, polling consumes CPU. If you set the poll interval to 1ms, you’ll choke the thread and slow down the actual application processing. 100ms-200ms is the sweet spot for integration tests. It feels instant to humans but gives the CPU breathing room.

Q: Can I use this for frontend testing?

Awaitility is a Java/JVM library. For frontend (JavaScript/TS), you should use the built-in `findBy` (which waits) in React Testing Library or `cy.get()` in Cypress, which implement the same “retry-until-true” philosophy.

Conclusion

Asynchronous architectures are the standard in 2026, but they brought testing headaches. `Thread.sleep` is a band-aid that eventually falls off, leaving you with a broken build pipeline.

Awaitility tests fixes the root cause: it synchronizes your test state with your system state. If you want a green CI dashboard on Monday morning, refactor your sleeps today.

Govind

For over 15 years, I have worked as a hands-on Java Architect and Senior Engineer, specializing in building and scaling high-performance, enterprise-level applications. My career has been focused primarily within the FinTech, Telecommunications, or E-commerce sector, where I’ve led teams in designing systems that handle millions of transactions per day.

Checkout my profile here : AUTHOR https://simplifiedlearningblog.com/author/

Related

Spring Boot Tags:Testing Asynchronous Flows with Awaitility

Post navigation

Previous Post: Migrating from Java 8/11 to Java 25: The Refactoring Checklist (2026 Edition)

More Related Articles

Spring Boot Application.properties vs. YAML: The 2026 Architect’s Verdict Spring Boot
How to Handle Errors in Spring Boot REST APIs (2026 Guide): The Global Exception Handling Pattern Spring Boot
Spring MVC vs. Spring Boot in 2026: The Senior Architect’s Definitive Guide Spring Boot

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recent Posts

  • Testing Asynchronous Flows with Awaitility: The End of Flaky Tests
  • Migrating from Java 8/11 to Java 25: The Refactoring Checklist (2026 Edition)
  • How to Handle Errors in Spring Boot REST APIs (2026 Guide): The Global Exception Handling Pattern
  • Practical System Design Examples using Spring Boot, Queues, and Caches (2026 Guide)
  • Spring Boot Application.properties vs. YAML: The 2026 Architect’s Verdict

Recent Comments

  1. Govind on Performance Principles of Software Architecture
  2. Gajanan Pise on Performance Principles of Software Architecture
Simplified Learning

Demystifying complex enterprise architecture for senior engineers. Practical guides on Java, Spring Boot, and Cloud Native systems.

Explore

  • Home
  • About Us
  • Author Profile: Govind
  • Contact Us

Legal

  • Privacy Policy
  • Terms and Conditions
  • Disclaimer
© 2026 Simplified Learning Blog. All rights reserved.
We use cookies to improve your experience and personalize ads. By continuing, you agree to our Privacy Policy and use of cookies.

Powered by PressBook Green WordPress theme