Skip to content

Adding backpressure design pattern #3233 #3249

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 12, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions backpressure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
---
title: "Backpressure Pattern in Java: controlling data streams from producers to consumer inorder to prevent overwhelming the consumer"
shortTitle: Backpressure
description: "Explore the Backpressure design pattern in Java with detailed examples. Learn how it helps by preventing system overload, ensuring stability and optimal performance by matching data flow to the consumer’s processing capability."
category: Concurrency
language: en
tag:
- Decoupling
- Event-driven
- Reactive
---

## Intent of the Backpressure Design Pattern

The Backpressure Design Pattern is a strategy used in software systems (especially in data streaming, reactive programming, and distributed systems)
to handle situations where a fast producer overwhelms a slow consumer. The intent is to prevent system instability, resource exhaustion, or crashes by managing the flow of data between components.

## Detailed Explanation of Backpressure Pattern with Real-World Examples

### Real-world examples

#### 1. Real-Time Data Streaming (Reactive Systems)
- **Stock Market Data**
- High-frequency trading systems generate millions of price updates per second, but analytics engines can't process them all in real time.
- Backpressure mechanisms (e.g., in RxJava, Akka, Reactor) throttle or drop excess data to avoid overwhelming downstream systems.
- **IoT Sensor DataQ**
- Thousands of IoT devices (e.g., smart factories, wearables) send continuous telemetry, but cloud processing has limited capacity.
- Reactive frameworks apply backpressure to buffer, drop, or slow down data emission.

#### 2. Message Queues (Kafka, RabbitMQ)
- **E-Commerce Order Processing**
- During flash sales (e.g., Black Friday), order requests spike, but payment and inventory systems can’t keep up.
- Message queues like Kafka and RabbitMQ use, Limited queue sizes to drop or reject messages when full or Consumer acknowledgments to slow producers if consumers lag.
- **Log Aggregation**
- Microservices generate massive logs, but centralized logging (E.g.: ELK Stack) can’t ingest them all at once.
- Kafka applies backpressure by pausing producers when consumers are slow.

#### 3. Stream Processing (Apache Flink, Spark)
- **Social Media Trends (Twitter, TikTok)**
- Viral posts create sudden spikes in data, but trend analysis is computationally expensive.
- Backpressure in Spark Streaming prioritizes recent data and discards older, less relevant updates.
- **Fraud Detection in Banking**
- Millions of transactions flow in, but fraud detection models take time to analyze each one.
- slow down ingestion if processing lags (Throttling), save progress to recover from backpressure-induced delays (Checkpointing).

### In plain words

The Backpressure design pattern is a flow control mechanism that prevents overwhelming a system by regulating data production based on the consumer’s processing capacity.

### Wikipedia says

Back pressure (or backpressure) is the term for a resistance to the desired flow of fluid through pipes. Obstructions or tight bends create backpressure via friction loss and pressure drop.

In distributed systems in particular event-driven architecture, back pressure is a technique to regulate flow of data, ensuring that components do not become overwhelmed.

### Architectural Diagram
![backpressure](./etc/backpressure.png)

## Programmatic Example of Backpressure Pattern in Java

First we will create a publisher that generates a data stream.
This publisher can generate a stream of integers.

```java
public class Publisher {
public static Flux<Integer> publish(int start, int count, int delay) {
return Flux.range(start, count).delayElements(Duration.ofMillis(delay)).log();
}
}
```

Then we can create a custom subscriber based on reactor BaseSubscriber.
It will take 500ms to process one item to simulate slow processing.
This subscriber will override following methods to apply backpressure on the publisher.
- hookOnSubscribe method and initially request for 10 items
- hookOnNext method which will process 5 items and request for 5 more items

```java
public class Subscriber extends BaseSubscriber<Integer> {

private static final Logger logger = LoggerFactory.getLogger(Subscriber.class);

@Override
protected void hookOnSubscribe(@NonNull Subscription subscription) {
logger.info("subscribe()");
request(10); //request 10 items initially
}

@Override
protected void hookOnNext(@NonNull Integer value) {
processItem();
logger.info("process({})", value);
if (value % 5 == 0) {
// request for the next 5 items after processing first 5
request(5);
}
}

@Override
protected void hookOnComplete() {
//completed processing.
}

private void processItem() {
try {
Thread.sleep(500); // simulate slow processing
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
}
}
```

Then we can create the stream using the publisher and subscribe to that stream.

```java
public static void main(String[] args) throws InterruptedException {
Subscriber sub = new Subscriber();
Publisher.publish(1, 8, 200).subscribe(sub);
Thread.sleep(5000); //wait for execution

}
```

Program output:

```
23:09:55.746 [main] DEBUG reactor.util.Loggers -- Using Slf4j logging framework
23:09:55.762 [main] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- onSubscribe(FluxConcatMapNoPrefetch.FluxConcatMapNoPrefetchSubscriber)
23:09:55.762 [main] INFO com.iluwatar.backpressure.Subscriber -- subscribe()
23:09:55.763 [main] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- request(10)
23:09:55.969 [parallel-1] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- onNext(1)
23:09:56.475 [parallel-1] INFO com.iluwatar.backpressure.Subscriber -- process(1)
23:09:56.680 [parallel-2] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- onNext(2)
23:09:57.185 [parallel-2] INFO com.iluwatar.backpressure.Subscriber -- process(2)
23:09:57.389 [parallel-3] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- onNext(3)
23:09:57.894 [parallel-3] INFO com.iluwatar.backpressure.Subscriber -- process(3)
23:09:58.099 [parallel-4] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- onNext(4)
23:09:58.599 [parallel-4] INFO com.iluwatar.backpressure.Subscriber -- process(4)
23:09:58.805 [parallel-5] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- onNext(5)
23:09:59.311 [parallel-5] INFO com.iluwatar.backpressure.Subscriber -- process(5)
23:09:59.311 [parallel-5] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- request(5)
23:09:59.516 [parallel-6] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- onNext(6)
23:10:00.018 [parallel-6] INFO com.iluwatar.backpressure.Subscriber -- process(6)
23:10:00.223 [parallel-7] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- onNext(7)
23:10:00.729 [parallel-7] INFO com.iluwatar.backpressure.Subscriber -- process(7)
23:10:00.930 [parallel-8] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- onNext(8)
23:10:01.436 [parallel-8] INFO com.iluwatar.backpressure.Subscriber -- process(8)
23:10:01.437 [parallel-8] INFO reactor.Flux.ConcatMapNoPrefetch.1 -- onComplete()
```

## When to Use the Backpressure Pattern

- Producers Are Faster Than Consumers
- If a producer generates data at a much faster rate than the consumer can handle, backpressure prevents resource overload.
- Example: A server emitting events 10x faster than the client can process.

- There’s Limited Memory or Resource Capacity
- Without flow control, queues or buffers can grow indefinitely, leading to out-of-memory errors or system crashes.
- Example: Streaming large datasets into a low-memory microservice.

- Building Reactive or Event-Driven Architectures
- Reactive systems thrive on non-blocking, asynchronous flows—and backpressure is a core component of the Reactive Streams specification.
- Example: Using RxJava, Project Reactor, Akka Streams, or Node.js streams.

- Unpredictable Workloads
- If the rate of data production or consumption can vary, backpressure helps adapt dynamically.
- Example: APIs receiving unpredictable spikes in traffic.

- Need to Avoid Data Loss or Overflow
- Instead of dropping data arbitrarily, backpressure lets you control flow intentionally.
- Example: Video or audio processing pipelines where dropping frames is costly.

## When to avoid the Backpressure Pattern

- For batch processing or simple linear flows with well-matched speeds.
- If data loss is acceptable and simpler strategies like buffering or throttling are easier to manage.
- When using fire-and-forget patterns (e.g., log shipping with retries instead of slowing the producer).

## Benefits and Trade-offs of Backpressure Pattern

### Benefits:

- Improved System Stability
- Prevents overload by controlling data flow.
- Reduces chances of out-of-memory errors, thread exhaustion, or service crashes.
- Efficient Resource Usage
- Avoids excessive buffering and unnecessary computation.
- Enables systems to do only the work they can handle.
- Better Responsiveness
- Keeps queues short, which improves latency and throughput.
- More consistent performance under load.
- Graceful Degradation
- If the system can't keep up, it slows down cleanly rather than failing unpredictably.
- Consumers get a chance to control the pace, leading to predictable behavior.
- Fits Reactive Programming
- It's essential in Reactive Streams, RxJava, Project Reactor, and Akka Streams.
- Enables composing async streams safely and effectively.

### Trade-offs:

- Complexity in Debugging
- Adds logic for flow control, demand signaling, and failure handling.
- More state to manage (e.g., request counts, pause/resume, buffer sizes).
- Harder Debugging & Testing
- Asynchronous flow + demand coordination = trickier to test and debug.
- Race conditions or deadlocks may occur if not handled carefully.
- Potential for Bottlenecks
- A slow consumer can throttle the entire system, even if other parts are fast.
- Needs smart handling (e.g., buffer + drop + retry strategies).

## Related Java Design Patterns
* [Publish-Subscribe Pattern](https://github.com/sanurah/java-design-patterns/blob/master/publish-subscribe/): Pub-Sub pattern decouples producers from consumers so they can communicate without knowing about each other. Backpressure manages flow control between producer and consumer to avoid overwhelming the consumer.
* [Observer Pattern](https://github.com/sanurah/java-design-patterns/blob/master/observer/): Both involve a producer (subject/publisher) notifying consumers (observers/subscribers). Observer is synchronous & tightly coupled (observers know the subject). Pub-Sub is asynchronous & decoupled (via a message broker).
* [Mediator Pattern](https://github.com/sanurah/java-design-patterns/blob/master/mediator/): A mediator centralizes communication between components (like a message broker in Pub-Sub). Mediator focuses on reducing direct dependencies between objects. Pub-Sub focuses on broadcasting events to unknown subscribers.

## References and Credits

* [Reactive Streams Specification](https://www.reactive-streams.org/)
* [Reactive Programming with RxJava by Tomasz Nurkiewicz & Ben Christensen](https://www.oreilly.com/library/view/reactive-programming-with/9781491931646/)
* [RedHat Developers Blog](https://developers.redhat.com/articles/backpressure-explained)
Binary file added backpressure/etc/backpressure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions backpressure/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.iluwatar</groupId>
<artifactId>java-design-patterns</artifactId>
<version>1.26.0-SNAPSHOT</version>
</parent>

<artifactId>backpressure</artifactId>

<dependencies>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.8.0-M1</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<version>3.8.0-M1</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
54 changes: 54 additions & 0 deletions backpressure/src/main/java/com/iluwatar/backpressure/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.iluwatar.backpressure;

import java.util.concurrent.CountDownLatch;
import lombok.extern.slf4j.Slf4j;

/**
* The Backpressure pattern is a flow control mechanism. It allows a consumer to signal to a
* producer to slow down or stop sending data when it's overwhelmed.
* <li>Prevents memory overflow, CPU thrashing, and resource exhaustion.
* <li>Ensures fair usage of resources in distributed systems.
* <li>Avoids buffer bloat and latency spikes. Key concepts of this design paradigm involves
* <li>Publisher/Producer: Generates data.
* <li>Subscriber/Consumer: Receives and processes data.
*
* <p>In this example we will create a {@link Publisher} and a {@link Subscriber}. Publisher
* will emit a stream of integer values with a predefined delay. Subscriber takes 500 ms to
* process one integer. Since the subscriber can't process the items fast enough we apply
* backpressure to the publisher so that it will request 10 items first, process 5 items and
* request for the next 5 again. After processing 5 items subscriber will keep requesting for
* another 5 until the stream ends.
*/
@Slf4j
public class App {

protected static CountDownLatch latch;

/**
* Program entry point.
*
* @param args command line args
*/
public static void main(String[] args) throws InterruptedException {

/*
* This custom subscriber applies backpressure:
* - Has a processing delay of 0.5 milliseconds
* - Requests 10 items initially
* - Process 5 items and request for the next 5 items
*/
Subscriber sub = new Subscriber();
// slow publisher emit 15 numbers with a delay of 200 milliseconds
Publisher.publish(1, 17, 200).subscribe(sub);

latch = new CountDownLatch(1);
latch.await();

sub = new Subscriber();
// fast publisher emit 15 numbers with a delay of 1 millisecond
Publisher.publish(1, 17, 1).subscribe(sub);

latch = new CountDownLatch(1);
latch.await();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.iluwatar.backpressure;

import java.time.Duration;
import reactor.core.publisher.Flux;

/** This class is the publisher that generates the data stream. */
public class Publisher {

/**
* On message method will trigger when the subscribed event is published.
*
* @param start starting integer
* @param count how many integers to emit
* @param delay delay between each item in milliseconds
* @return a flux stream of integers
*/
public static Flux<Integer> publish(int start, int count, int delay) {
return Flux.range(start, count).delayElements(Duration.ofMillis(delay)).log();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.iluwatar.backpressure;

import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.BaseSubscriber;

/** This class is the custom subscriber that subscribes to the data stream. */
@Slf4j
public class Subscriber extends BaseSubscriber<Integer> {

private static final Logger logger = LoggerFactory.getLogger(Subscriber.class);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to declare logger like this. With the Lombok annotation you automatically have LOGGER available.


@Override
protected void hookOnSubscribe(@NonNull Subscription subscription) {
request(10); // request 10 items initially
}

@Override
protected void hookOnNext(@NonNull Integer value) {
processItem();
logger.info("process({})", value);
if (value % 5 == 0) {
// request for the next 5 items after processing first 5
request(5);
}
}

@Override
protected void hookOnComplete() {
App.latch.countDown();
}

private void processItem() {
try {
Thread.sleep(500); // simulate slow processing
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
}
}
13 changes: 13 additions & 0 deletions backpressure/src/test/java/com/iluwatar/backpressure/AppTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.iluwatar.backpressure;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

import org.junit.jupiter.api.Test;

public class AppTest {

@Test
void shouldExecuteApplicationWithoutException() {
assertDoesNotThrow(() -> App.main(new String[] {}));
}
}
Loading
Loading