Quarkus

Introduction

Anyone who develops microservices knows: failures happen. Dropped connections, slow external services, congested queues — all of this can compromise a system’s availability.

Quarkus, in addition to being fast and optimized for native Kubernetes environments, already comes with support for resilience features through MicroProfile Fault Tolerance. This means we can apply classic fault tolerance patterns to our applications with just a few annotations.

In this article, I’ll show you how to implement these patterns practically, with code examples and tips that can save your application in production environments.

Understanding MicroProfile Fault Tolerance

MicroProfile Fault Tolerance is a specification designed for Java microservices. It allows you to add failure protection mechanisms without having to write all the logic manually.

Among the available features are:

  • 🔄 Retry: repeats execution when transient failures occur.
  • ⏱️ Timeout: defines the maximum response time.
  • 🚦 Circuit Breaker: cuts calls to an unstable service, protecting the application.
  • 🛠️ Fallback: offers an alternative when the main logic fails.
  • 🧱 Bulkhead: controls the number of concurrent requests to avoid saturation.

To use it in Quarkus, simply include the extension:

./mvnw quarkus:add-extension -Dextensions="smallrye-fault-tolerance"

Practical Usage Examples

Retry – Extra Attempts

When an external integration fails due to momentary instability, repeating the call can solve the issue.

Retry Pattern
@Retry(maxRetries = 3, delay = 500)
public String processTransaction() {
    if (Math.random() > 0.5) {
        throw new RuntimeException("Temporary provider error!");
    }
    return "Transaction completed.";
}

👉 In this example, the operation will be re-executed up to 3 times, with a 500 ms interval between attempts.

Timeout – Setting Wait Limits

It’s not healthy to leave your application stuck in calls that never finish.

Timeout Pattern
@Timeout(2000)
public String generateReport() {
    try {
        Thread.sleep(4000); // Simulates slowness
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return "Report generated.";
}

👉 If the operation takes more than 2 seconds, it will be interrupted and the application continues.

Circuit Breaker – Avoiding Cascade Failures

An unstable service can cause overload if it continues to be called repeatedly.

Circuit Breaker Pattern
@CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 5000)
public String consultCatalog() {
    if (Math.random() > 0.7) {
        throw new RuntimeException("Product catalog failure!");
    }
    return "Product available.";
}

👉 Here, if half of the last 4 calls fail, the circuit “opens” and the next calls fail fast for 5 seconds, without trying to access the service.

Fallback – A Smart Plan B

When everything goes wrong, it’s still possible to respond usefully to the user.

Fallback Pattern
@Fallback(fallbackMethod = "getPriceCached")
public String getPrice() {
    throw new RuntimeException("Price service unavailable!");
}

public String getPriceCached() {
    return "Price not available at the moment. Estimated value: $120.00";
}

👉 In this case, if the service is down, an alternative value is returned to the client.

Bulkhead – Concurrency Control

If a part of the system is overloaded by simultaneous requests, it can freeze the whole system. Bulkhead helps isolate this impact.

Bulkhead Pattern

Bulkhead – Controle de Concorrência

@Bulkhead(value = 5)
public String processOrder() {
    return "Order processed successfully.";
}

👉 Only 5 parallel executions will be allowed. The rest will be queued or rejected, avoiding resource saturation.

Tips and Best Practices

  • Combine strategies: for example, @Timeout together with @Fallback ensures that the user always gets a response.
  • Be careful with excessive retries: repeating calls without control can increase the load on already compromised services.
  • 📊 Monitor behavior: use metrics exposed by Quarkus (via MicroProfile Metrics) and integrate with Prometheus/Grafana.
  • 🧪 Test failure scenarios: simulate database unavailability, slowness in external APIs and see how the application reacts.

Conclusion

Designing distributed systems requires accepting that failures will happen. What differentiates a fragile application from a robust one is the ability to recover and continue operating.

With native support for MicroProfile Fault Tolerance, Quarkus offers practical tools to implement resilience in a simple and efficient way. Using annotations like @Retry, @Timeout, @CircuitBreaker, @Fallback and @Bulkhead, it’s possible to create more reliable microservices ready to run in complex environments like the cloud.

In summary: prepare your system to fail — because sooner or later, it will happen.