Mastering Spring Reactive Programming
From the Reactive Manifesto to Project Reactor, Spring WebFlux, and production-ready patterns โ all with Kotlin code examples.
Introducing Reactive Programming
Reactive programming builds systems that react to data as it flows rather than waiting for results. Instead of blocking threads, reactive programs describe transformation pipelines that execute when data becomes available.
Declarative programming with an emphasis on asynchronous data streams and change propagation โ data types express values that change over time, and downstream computations update automatically.
The core abstraction is the Observable/Subscriber pattern. An Observable is a data source where observers register interest. Subscribers receive updates whenever the subject changes โ the source keeps track of its observers and pushes notifications automatically.
// The Observable/Subscriber pattern โ foundation of reactive programming// Observable: a data source that emits values over time// Subscribers register interest and receive updates automaticallyval priceStream: Flux<Double> = stockService.watchPrice("AAPL")// Subscriber 1: Dashboard UIpriceStream.subscribe { price ->println("Dashboard updated: $$price")}// Subscriber 2: Alert systempriceStream.filter { it > 200.0 }.subscribe { price ->alertService.notify("AAPL crossed $$200: $$price")}// Both subscribers react to the SAME stream independently// The Observable keeps track of its observers and pushes updates

Observer Pattern โ Source: Wikipedia

Train/Signal analogy โ Control Room (Observable) sends signals; Train (Subscriber) reacts
The Three Principles
A database call returns immediately rather than blocking the calling thread. The system handles more work with fewer resources.
Data flows through a pipeline of transformations. Every value sequence is an observable that operators can filter, map, and combine.
When a value changes upstream, all dependent downstream computations re-evaluate automatically โ the "spreadsheet model" applied to async streams.
// The three principles in action// 1. ASYNCHRONOUS โ non-blocking, immediate returnfun fetchUserData(id: String): Mono<UserData> {// Returns immediately โ does NOT block the threadreturn userRepository.findById(id) // async DB call}// 2. STREAMS โ data flows through a pipeline of transformationsfun processOrders(): Flux<OrderSummary> {return orderRepository.findAll() // source stream.filter { it.status == "ACTIVE" } // filter stage.map { it.toSummary() } // transform stage.sort(compareByDescending { it.total }) // sort stage}// 3. PROPAGATION OF CHANGE โ downstream reacts to upstream changesfun liveDashboard(): Flux<DashboardState> {return Flux.combineLatest(userService.activeUserCount(), // changes propagateorderService.revenueStream(), // changes propagatemetricService.healthCheck() // changes propagate) { users, revenue, health ->DashboardState(users, revenue, health) // auto-updated!}}
Imperative vs Reactive: From "pull" (blocking) to "push" (non-blocking, event-driven):
What to notice
- Each findById() call blocks the thread until the DB responds
- Total time = sum of all latencies (e.g. 200ms + 300ms = 500ms)
- Thread is wasted waiting โ cannot serve other requests
// Imperative (blocking) approachfun getUser(id: String): User {val user = userRepository.findById(id) // blocks threadval orders = orderRepository.findByUserId(id) // blocks againreturn user.copy(orders = orders)// Thread is IDLE during both calls// Total time = sum of both latencies}
What to notice
- Returns Mono immediately โ thread is free to handle other work
- zipWith() fires both queries in parallel
- Total time = slowest query (e.g. max(200ms, 300ms) = 300ms)
// Reactive (non-blocking) approachfun getUser(id: String): Mono<User> {return userRepository.findById(id) // non-blocking.zipWith(orderRepository.findByUserId(id).collectList()).map { (user, orders) -> user.copy(orders = orders) }// Thread is FREE to handle other requests// Both queries execute in parallel with zipWith}
The Reactive Manifesto
Four architectural pillars that Spring WebFlux and Project Reactor are built upon:

Reliable upper bounds for response times. Consistent quality of service under all conditions.
Built for failures โ replication, containment, isolation. Each component fails independently without cascading.
Scale up and down in response to load changes. No contention points or central bottlenecks.
Asynchronous message passing for loose coupling and location transparency. Recipients consume resources only while active.
Reactive Streams Specification
A specification (part of java.util.concurrent.Flow in Java 9+) defining the contract for async stream processing with non-blocking backpressure. Four core interfaces:
// The four core interfaces of Reactive Streams specificationinterface Publisher<T> {fun subscribe(subscriber: Subscriber<in T>)}interface Subscriber<T> {fun onSubscribe(subscription: Subscription)fun onNext(item: T)fun onError(throwable: Throwable)fun onComplete()}interface Subscription {fun request(n: Long) // backpressure: request N itemsfun cancel()}interface Processor<T, R> : Subscriber<T>, Publisher<R>
Publisher produces โ Subscriber consumes โ Subscription enables backpressure via request(n) โ Processor is both.
Spring Framework Foundations
The foundations that reactive Spring builds upon: IoC and Dependency Injection.
IoC & Dependency Injection
IoC transfers control from developers to a container. DI wires objects together โ decoupling implementation from execution and simplifying testing.

Spring Framework Runtime โ Data Access/Integration, Web, AOP, Core Container, Test
// Inversion of Control (IoC) & Dependency Injection (DI)// Spring manages object creation and wiring โ you declare, Spring delivers@Configurationclass AppConfig {@Beanfun userRepository(): UserRepository = InMemoryUserRepository()@Beanfun userService(repo: UserRepository): UserService = UserService(repo)}// Constructor injection (preferred) โ Spring resolves dependencies automatically@Serviceclass OrderService(private val userRepo: UserRepository,private val productRepo: ProductRepository,private val notificationService: NotificationService) {fun placeOrder(userId: String, productId: String): Mono<Order> {return userRepo.findById(userId).zipWith(productRepo.findById(productId)).flatMap { (user, product) ->val order = Order(user = user, product = product)notificationService.notify(user, order).thenReturn(order)}}}
Spring Framework 6 Key Changes
Spring Framework 6 introduced significant changes that affect how we build reactive applications:

Spring Boot architecture โ Application layer built on Spring MVC, Data, ORM with Dependency Injection
// Spring Framework 6 โ Key Changes// 1. Baseline Java 17 (minimum JDK version)// 2. Jakarta EE namespace migration: javax.* โ jakarta.*import jakarta.servlet.http.HttpServletRequest // was javax.servletimport jakarta.persistence.Entity // was javax.persistence// 3. AOT (Ahead-Of-Time) Processing โ faster startup// Build-time pre-processing of bean definitions// Enabled via: ./gradlew nativeCompile// 4. GraalVM Native Executables// plugins { id("org.graalvm.buildtools.native") }// Produces standalone binary: ~50ms startup vs ~2s JVM// 5. HttpMethod changed from Enum to Classval method = HttpMethod.valueOf("PATCH")// 6. Servers: Tomcat 10, Jetty 11, Netty (default for WebFlux)// 7. @Controller required (type-level @RequestMapping alone not enough)@Controller // NOW required@RequestMapping("/api")class ApiController { }
RxJava & Project Reactor
RxJava popularized reactive on the JVM. Project Reactor is the 4th-gen library based on Reactive Streams โ Spring's native choice.
// RxJava โ the foundation that inspired Project Reactorimport io.reactivex.rxjava3.core.Observable// Observable.just() โ emit a single itemval signal: Observable<String> = Observable.just("Red Signal!")signal.subscribe { println(it) } // Red Signal!// Observable.fromIterable() โ emit items one by oneval colors = Observable.fromIterable(listOf("Red", "Green", "Blue"))colors.subscribe({ item -> println("onNext: $item") },{ error -> println("onError: $error") },{ println("onComplete!") })// onNext: Red// onNext: Green// onNext: Blue// onComplete!// Reactor (Project Reactor) is the 4th-gen reactive library// used by Spring WebFlux. It implements Reactive Streams spec.// Two core types: Mono<T> (0..1) and Flux<T> (0..N)
RxJava 3: Observable, Single, Maybe, Flowable. Only Flowable implements Reactive Streams.
Reactor: Flux (0..N), Mono (0..1). Full Reactive Streams compliance. Spring-native.
Mono & Flux: The Building Blocks
Two core types that implement the Publisher interface in Project Reactor:
Mono<T> โ 0 or 1 Element
A reactive Optional โ emits at most one item. Perfect for single-value operations like findById.

Mono marble diagram โ Single item emission through operator, success (|) or error (X)
import reactor.core.publisher.Mono// Mono<T> = 0 or 1 element (like Optional but async)// Create a Mono with a valueval greeting: Mono<String> = Mono.just("Hello Reactive World")// Create an empty Monoval empty: Mono<String> = Mono.empty()// Create from nullable (safe)val safe: Mono<String> = Mono.justOrEmpty(null as String?)// Create from a Callable (deferred execution)val deferred: Mono<String> = Mono.fromCallable {expensiveComputation()}// Subscribe to consume the valuegreeting.subscribe { value -> println(value) }// Output: Hello Reactive World
Flux<T> โ 0 to N Elements
A stream of 0 to N elements. Use for collections, event streams, or multiple values over time.

Flux marble diagram โ Multiple items emitted through operator transformations
import reactor.core.publisher.Flux// Flux<T> = 0 to N elements (like a reactive List/Stream)// Create from valuesval numbers: Flux<Int> = Flux.just(1, 2, 3, 4, 5)// Create from a collectionval fromList: Flux<String> = Flux.fromIterable(listOf("Spring", "Reactor", "WebFlux"))// Create a rangeval range: Flux<Int> = Flux.range(1, 10)// Create an infinite stream (emits every 500ms)val interval: Flux<Long> = Flux.interval(Duration.ofMillis(500))// Operators: filter, map, takenumbers.filter { it % 2 == 0 }.map { it * 10 }.subscribe { println(it) }// Output: 20, 40
Hot vs Cold Publishers
Cold: generates data anew per subscription. Hot: emits regardless of subscribers โ late joiners miss earlier events.
import reactor.core.publisher.Fluximport java.time.Duration// โโโ COLD Publisher โโโ// Generates data anew for EACH subscriber// Nothing happens until subscribe()val coldFlux = Flux.just("A", "B", "C")coldFlux.subscribe { println("Subscriber 1: $it") } // A, B, CcoldFlux.subscribe { println("Subscriber 2: $it") } // A, B, C (fresh)// โโโ HOT Publisher โโโ// Emits regardless of subscribers// Late subscribers miss earlier eventsval hotFlux = Flux.interval(Duration.ofSeconds(1)).share() // convert cold โ hot (share among subscribers)hotFlux.subscribe { println("Sub 1: $it") } // 0, 1, 2, 3...Thread.sleep(3000)hotFlux.subscribe { println("Sub 2: $it") } // 3, 4, 5... (missed 0,1,2)// ConnectableFlux โ hot publisher with manual connectval connectable = Flux.range(1, 5).publish()connectable.subscribe { println("Sub A: $it") }connectable.subscribe { println("Sub B: $it") }connectable.connect() // NOW both subscribers start receiving
Cold: Database queries, HTTP requests, file reads โ each subscriber gets its own fresh data. Hot: WebSocket streams, Kafka topics, sensor data, stock tickers โ data flows regardless of who is listening. Use .share() or .publish().autoConnect() to convert cold to hot.
Lazy vs Eager Loading
Mono.just() evaluates eagerly. Mono.defer() and fromCallable() evaluate lazily โ only when subscribed.
import reactor.core.publisher.Mono// โโโ EAGER Loading (Mono.just) โโโ// The value is computed IMMEDIATELY, even before subscribefun eagerExample() {val eagerMono = Mono.just(expensiveQuery()) // query runs NOW!// Even if we never subscribe, the query already executed}// โโโ LAZY Loading (Mono.defer) โโโ// The value is computed ONLY when someone subscribesfun lazyExample() {val lazyMono = Mono.defer {println("Query triggered!")Mono.just(expensiveQuery()) // runs only on subscribe()}// Nothing happens yet...lazyMono.subscribe { println("Got: $it") } // NOW the query runs}// โโโ Mono.fromCallable (also lazy) โโโval callable = Mono.fromCallable { computeValue() }// Equivalent to defer but for simple Callable functions
Use Mono.just() for values already available. Use Mono.defer() or Mono.fromCallable() for values that require computation, I/O, or side effects โ you want those to execute only when the pipeline is actually subscribed.
Backpressure
The mechanism by which a consumer signals to the producer how much data it can handle. Without it, fast producers overwhelm slow consumers.
import reactor.core.publisher.Fluximport org.reactivestreams.Subscription// Backpressure: the consumer controls the flow rateFlux.range(1, 1000).doOnNext { println("Produced: $it") }.subscribe(object : BaseSubscriber<Int>() {override fun hookOnSubscribe(subscription: Subscription) {// Request only 5 items at a timerequest(5)}override fun hookOnNext(value: Int) {println("Consumed: $value")if (value % 5 == 0) {request(5) // Request next batch of 5}}})// Backpressure strategies:// .onBackpressureBuffer() โ buffer excess items// .onBackpressureDrop() โ drop items consumer can't keep up with// .onBackpressureLatest() โ keep only the latest item
In production, backpressure prevents your application from crashing when a database query returns millions of rows, when an external API sends data faster than you can process it, or when thousands of WebSocket clients flood your server with messages.
onBackpressureBuffer() โ Buffer in memory ยท onBackpressureDrop() โ Drop excess ยท onBackpressureLatest() โ Keep only latest ยท onBackpressureError() โ Signal error. Always set a max buffer size in production.
Reactive Operators
The vocabulary of reactive programming โ transform, filter, combine, and control data streams declaratively.

Reactor operator pipeline โ fromIterable โ doOnNext โ transform (filter, map) โ subscriber chain
import reactor.core.publisher.Fluximport reactor.core.publisher.Mono// โโโ Transformation Operators โโโ// map: synchronous 1-to-1 transformationFlux.just("spring", "reactor", "webflux").map { it.uppercase() }.subscribe { println(it) }// SPRING, REACTOR, WEBFLUX// flatMap: async transformation (returns Publisher)fun findUserByName(name: String): Mono<User> = userRepo.findByName(name)Flux.just("Alice", "Bob").flatMap { name -> findUserByName(name) }.subscribe { println(it) }// โโโ Filtering Operators โโโFlux.range(1, 20).filter { it % 3 == 0 } // 3, 6, 9, 12, 15, 18.take(3) // 3, 6, 9.subscribe { println(it) }// โโโ Combining Operators โโโ// zip: combine element-by-elementval names = Flux.just("Alice", "Bob")val ages = Flux.just(30, 25)Flux.zip(names, ages) { name, age -> "$name is $age" }.subscribe { println(it) }// Alice is 30, Bob is 25// merge: interleave multiple sources (no ordering guarantee)Flux.merge(fastSource, slowSource).subscribe { println(it) }
Advanced Operators
Production operators: reusable transformations, batching, caching, grouping, and recursive expansion.
transform, buffer, cache, groupBy
import reactor.core.publisher.Fluximport java.time.Duration// โโโ transform: reusable operator chains โโโval filterAndUppercase: (Flux<String>) -> Flux<String> = { flux ->flux.filter { it != "spam" }.map { it.uppercase() }}Flux.just("hello", "spam", "world").transform(filterAndUppercase).subscribe { println(it) } // HELLO, WORLD// โโโ buffer: collect into batches โโโFlux.range(1, 10).buffer(3).subscribe { println(it) } // [1,2,3], [4,5,6], [7,8,9], [10]// โโโ cache: replay for late subscribers โโโval cached = Flux.just(1, 2, 3).doOnNext { println("Producing: $it") }.cache() // only produces once, replays for all subscriberscached.subscribe { println("Sub1: $it") }cached.subscribe { println("Sub2: $it") } // replayed from cache// โโโ groupBy: partition by key โโโFlux.range(1, 10).groupBy { if (it % 2 == 0) "Even" else "Odd" }.flatMap { group ->group.collectList().map { "${group.key()}: $it" }}.subscribe { println(it) }// Even: [2, 4, 6, 8, 10]// Odd: [1, 3, 5, 7, 9]
concat, combineLatest, expand, limitRate
import reactor.core.publisher.Fluximport reactor.core.publisher.Monoimport java.time.Duration// โโโ concat: sequential (preserves order) โโโval first = Flux.just(1, 2, 3).delayElements(Duration.ofMillis(100))val second = Flux.just(4, 5, 6)Flux.concat(first, second).subscribe { print("$it ") } // 1 2 3 4 5 6 (always in order)// โโโ combineLatest: react to ANY source change โโโval temp = Flux.interval(Duration.ofSeconds(2)).map { "${20 + it}ยฐC" }val humidity = Flux.interval(Duration.ofSeconds(3)).map { "${60 + it}%" }Flux.combineLatest(temp, humidity) { t, h -> "Temp=$t Humidity=$h" }.subscribe { println(it) }// โโโ expand: recursive (tree/graph traversal) โโโdata class TreeNode(val id: Int, val children: List<Int>)fun fetchChildren(id: Int): Flux<TreeNode> = repository.findChildren(id)Mono.just(TreeNode(1, listOf(2, 3))).expand { node -> fetchChildren(node.id) }.subscribe { println("Visited: ${it.id}") }// โโโ delayElements + limitRate: throttle processing โโโFlux.range(1, 100).delayElements(Duration.ofMillis(50)).limitRate(10) // request 10 at a time (backpressure).subscribe { println(it) }// โโโ flatMapIterable: flatten collections โโโFlux.just(listOf(1, 2), listOf(3, 4), listOf(5)).flatMapIterable { it } // flattens to: 1, 2, 3, 4, 5.subscribe { print("$it ") }
Error Handling
Errors are first-class signals that propagate downstream and terminate the stream unless handled with recovery operators.
import reactor.core.publisher.Fluximport reactor.core.publisher.Monoimport reactor.util.retry.Retryimport java.time.Duration// โโโ Error Recovery Operators โโโ// onErrorReturn: provide a fallback valueMono.error<String>(RuntimeException("DB down")).onErrorReturn("Default Value").subscribe { println(it) } // Default Value// onErrorResume: switch to a fallback Publisherfun fetchFromPrimary(): Mono<Data> = primaryDb.find()fun fetchFromCache(): Mono<Data> = cache.find()fetchFromPrimary().onErrorResume { ex ->println("Primary failed: ${ex.message}")fetchFromCache() // fallback to cache}.subscribe { println(it) }// โโโ Retry with Backoff โโโexternalApi.call().retryWhen(Retry.backoff(3, Duration.ofSeconds(1)).maxBackoff(Duration.ofSeconds(10)).filter { it is ServiceUnavailableException }).onErrorReturn("Service temporarily unavailable").subscribe { println(it) }
Async Programming Patterns
Parallel processing, schedulers, and event-driven patterns with Reactor Sinks.
import reactor.core.publisher.Fluximport reactor.core.scheduler.Schedulers// โโโ Parallel processing with ParallelFlux โโโFlux.range(1, 100).parallel(4) // split into 4 rails.runOn(Schedulers.parallel()) // run on parallel scheduler.map { processItem(it) } // each rail processes independently.sequential() // merge back to single Flux.subscribe { println(it) }// โโโ Schedulers explained โโโ// Schedulers.parallel() โ CPU-bound work (cores count threads)// Schedulers.boundedElastic() โ I/O-bound / blocking work (capped pool)// Schedulers.single() โ single-threaded (sequential tasks)// Schedulers.immediate() โ current thread (no scheduling)// โโโ Real-time event generation โโโfun generateEvents(): Flux<String> =Flux.interval(Duration.ofSeconds(1)).map { index -> "Event $index" }.take(10) // emit 10 events then complete// โโโ Event-driven with Sinks โโโval sink = Sinks.many().multicast().onBackpressureBuffer<String>()// Publish events programmaticallysink.tryEmitNext("user.created")sink.tryEmitNext("order.placed")// Multiple subscribers receive eventssink.asFlux().subscribe { println("Listener 1: $it") }sink.asFlux().subscribe { println("Listener 2: $it") }
parallel() โ CPU-bound (cores count) ยท boundedElastic() โ I/O-bound (wrapping blocking calls) ยท single() โ sequential tasks ยท immediate() โ current thread.
Spring WebFlux
The reactive web framework โ runs on a non-blocking event loop (Netty) with two programming models: annotated controllers and functional routing.

Shared: @Controller, reactive clients ยท MVC: JDBC/JPA, thread-per-request ยท WebFlux: RouterFunction, Netty, Mono/Flux, backpressure.
Annotated Controllers
Same as Spring MVC but with Mono/Flux return types. The framework handles subscription and backpressure.
import org.springframework.web.bind.annotation.*import reactor.core.publisher.Fluximport reactor.core.publisher.Mono@RestController@RequestMapping("/api/products")class ProductController(private val service: ProductService) {@GetMappingfun getAllProducts(): Flux<Product> =service.findAll()@GetMapping("/{id}")fun getProduct(@PathVariable id: String): Mono<Product> =service.findById(id)@PostMappingfun createProduct(@RequestBody product: Product): Mono<Product> =service.create(product)@PutMapping("/{id}")fun updateProduct(@PathVariable id: String,@RequestBody product: Product): Mono<Product> =service.update(id, product)@DeleteMapping("/{id}")fun deleteProduct(@PathVariable id: String): Mono<Void> =service.delete(id)}
Functional Routing
Separates route definitions from handler logic. More composable and testable.
import org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.web.reactive.function.server.*// Functional Routing โ alternative to @RestController@Configurationclass ProductRouter {@Beanfun routes(handler: ProductHandler): RouterFunction<ServerResponse> =router {"/api/products".nest {GET("", handler::findAll)GET("/{id}", handler::findById)POST("", handler::create)PUT("/{id}", handler::update)DELETE("/{id}", handler::delete)}}}@Componentclass ProductHandler(private val service: ProductService) {fun findAll(request: ServerRequest): Mono<ServerResponse> =ServerResponse.ok().body(service.findAll(), Product::class.java)fun findById(request: ServerRequest): Mono<ServerResponse> {val id = request.pathVariable("id")return service.findById(id).flatMap { ServerResponse.ok().bodyValue(it) }.switchIfEmpty(ServerResponse.notFound().build())}fun create(request: ServerRequest): Mono<ServerResponse> =request.bodyToMono(Product::class.java).flatMap { service.create(it) }.flatMap { ServerResponse.created(URI("/api/products/${it.id}")).bodyValue(it) }}
WebClient
The non-blocking replacement for RestTemplate โ streaming, timeouts, retries built-in.
import org.springframework.web.reactive.function.client.WebClientimport reactor.core.publisher.Mono// WebClient: non-blocking HTTP client (replaces RestTemplate)@Serviceclass ExternalApiService(private val webClient: WebClient) {fun fetchUser(userId: String): Mono<UserDto> =webClient.get().uri("/users/{id}", userId).retrieve().onStatus(HttpStatusCode::is4xxClientError) { response ->Mono.error(UserNotFoundException(userId))}.bodyToMono(UserDto::class.java).timeout(Duration.ofSeconds(5)).retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))fun fetchAllOrders(): Flux<OrderDto> =webClient.get().uri("/orders").retrieve().bodyToFlux(OrderDto::class.java)}// WebClient Bean configuration@Configurationclass WebClientConfig {@Beanfun webClient(): WebClient =WebClient.builder().baseUrl("https://api.example.com").defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).build()}
WebClient vs RestTemplate
What to notice
- RestTemplate blocks the thread for the entire HTTP roundtrip
- One thread per request: 200 concurrent users = 200 blocked threads
- RestTemplate is in maintenance mode since Spring 5 (deprecated)
// RestTemplate (blocking, DEPRECATED)val restTemplate = RestTemplate()// Thread is BLOCKED during the entire HTTP callval user: User = restTemplate.getForObject("/users/1", User::class.java)!!// Thread waits here... doing nothing...val orders: List<Order> = restTemplate.getForObject("/orders?userId=1", typeRef<List<Order>>())!!// Blocked again... wasting resources// One thread per request model// 200 concurrent users = 200 threads blocked
What to notice
- WebClient returns Mono/Flux immediately โ thread goes back to the pool
- Small thread pool handles thousands of concurrent connections
- Built-in support for streaming (Flux), timeouts, retries, and backpressure
// WebClient (non-blocking, RECOMMENDED)val webClient = WebClient.create("http://api.example.com")// Thread is FREE during the HTTP callval user: Mono<User> = webClient.get().uri("/users/1").retrieve().bodyToMono(User::class.java)val orders: Flux<Order> = webClient.get().uri("/orders?userId=1").retrieve().bodyToFlux(Order::class.java)// Shared thread pool handles thousands of requests// 200 concurrent users = same small thread pool
Sequential vs Parallel Calls
What to notice
- flatMap chains create sequential dependency โ each call waits
- Total time = sum of ALL call latencies
// Sequential: each call waits for the previous onefun sequential(): Mono<String> {return webClient.get().uri("/user/1").retrieve().bodyToMono(User::class.java).flatMap { user ->webClient.get().uri("/orders/${user.id}").retrieve().bodyToMono(OrderList::class.java).map { orders ->"${user.name}: ${orders.size} orders"}}}// /user=200ms + /orders=300ms => total ~500ms
What to notice
- Mono.zip() fires all calls simultaneously and combines results
- Total time = slowest single call (massive speedup)
// Parallel: all calls fire simultaneouslyfun parallel(): Mono<Dashboard> {val user = webClient.get().uri("/user/1").retrieve().bodyToMono(User::class.java)val orders = webClient.get().uri("/orders").retrieve().bodyToFlux(Order::class.java).collectList()val notifications = webClient.get().uri("/notifications").retrieve().bodyToFlux(Notification::class.java).collectList()return Mono.zip(user, orders, notifications).map { (u, o, n) -> Dashboard(u, o, n) }}// 200ms, 300ms, 150ms => total ~300ms (parallel!)
Latency Optimization
import io.netty.resolver.DefaultAddressResolverGroupimport reactor.netty.http.client.HttpClientimport java.time.Duration// โโโ Connection Pooling (reuse connections) โโโval httpClient = HttpClient.create().resolver(DefaultAddressResolverGroup.INSTANCE) // DNS cachingval webClient = WebClient.builder().clientConnector(ReactorClientHttpConnector(httpClient)).baseUrl("https://api.example.com").build()// โโโ Timeout + Retry + Fallback โโโwebClient.get().uri("/data").retrieve().bodyToMono(Data::class.java).timeout(Duration.ofSeconds(3)) // fail after 3s.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) // retry 3x.onErrorReturn(Data.default()) // fallback value// โโโ Response Caching (Caffeine) โโโval cache = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(Duration.ofMinutes(5)).build<String, Data>()fun getData(key: String): Mono<Data> {val cached = cache.getIfPresent(key)if (cached != null) return Mono.just(cached)return webClient.get().uri("/data/$key").retrieve().bodyToMono(Data::class.java).doOnNext { cache.put(key, it) }}
Server-Sent Events (SSE)
Server pushes data to clients over a long-lived connection. A Flux naturally maps to an SSE stream.
import org.springframework.http.MediaTypeimport org.springframework.web.bind.annotation.*import reactor.core.publisher.Fluximport java.time.Duration// Server-Sent Events (SSE) โ real-time data streaming@RestControllerclass EventController(private val eventService: EventService) {@GetMapping(path = ["/events/stream"],produces = [MediaType.TEXT_EVENT_STREAM_VALUE])fun streamEvents(): Flux<ServerSentEvent<Event>> =eventService.getEventStream().map { event ->ServerSentEvent.builder(event).id(event.id).event(event.type).build()}// Heartbeat to keep the connection alive@GetMapping(path = ["/events/heartbeat"],produces = [MediaType.TEXT_EVENT_STREAM_VALUE])fun heartbeat(): Flux<String> =Flux.interval(Duration.ofSeconds(2)).map { "ping" }}
Real-time dashboards, live notifications, stock price tickers, chat applications, log streaming, CI/CD build status updates โ anywhere you need the server to push updates without the client polling.
Testing Reactive Code
StepVerifier from reactor-test โ assert elements, verify completion, test errors without blocking.
import reactor.test.StepVerifierimport reactor.core.publisher.Fluximport reactor.core.publisher.Mono// StepVerifier: the standard tool for testing reactive streams// Test a Mono@Testfun `should find product by id`() {val product = Product(id = "1", name = "Laptop", price = 999.99)`when`(repository.findById("1")).thenReturn(Mono.just(product))StepVerifier.create(service.findById("1")).expectNext(product).verifyComplete()}// Test a Flux@Testfun `should return all products`() {val products = listOf(Product("1", "Laptop", 999.99),Product("2", "Phone", 699.99))`when`(repository.findAll()).thenReturn(Flux.fromIterable(products))StepVerifier.create(service.findAll()).expectNextCount(2).verifyComplete()}// Test error handling@Testfun `should handle not found`() {`when`(repository.findById("999")).thenReturn(Mono.empty())StepVerifier.create(service.findById("999")).expectError(ProductNotFoundException::class.java).verify()}
Reactive Repositories
Non-blocking data access with R2DBC, MongoDB, and Couchbase โ returning Mono and Flux.
// โโโ R2DBC (PostgreSQL / MySQL) โโโ// Non-blocking alternative to JPA/JDBC@Table("users")data class User(@Id val id: Long? = null,val name: String,val email: String,val role: String)@Repositoryinterface UserRepository : ReactiveCrudRepository<User, Long> {fun findByEmail(email: String): Mono<User>fun findByRole(role: String): Flux<User>}// โโโ Reactive MongoDB โโโ@Document(collection = "products")data class Product(@Id val id: String? = null,val name: String,val price: Double,val category: String)@Repositoryinterface ProductRepository : ReactiveMongoRepository<Product, String> {fun findByCategory(category: String): Flux<Product>fun findByPriceLessThan(maxPrice: Double): Flux<Product>}// โโโ Reactive Couchbase โโโ@Documentdata class Session(@Id val id: String,val userId: String,@Expiry(expiry = 3600) val expiresIn: Int = 3600)@Repositoryinterface SessionRepository : ReactiveCouchbaseRepository<Session, String> {fun findByUserId(userId: String): Flux<Session>}
Concurrency Control
Optimistic locking with @Version โ no locks held across async boundaries.
// โโโ Optimistic Locking (version-based) โโโ// Multiple transactions proceed; check version before commit// If version mismatch โ retry (no locks held)@Document(collection = "inventory")data class Inventory(@Id val id: String? = null,val productId: String,val quantity: Int,@Version val version: Long = 0 // Optimistic lock field)@Serviceclass InventoryService(private val repo: InventoryRepository) {fun decrementStock(productId: String, amount: Int): Mono<Inventory> {return repo.findByProductId(productId).flatMap { inv ->if (inv.quantity < amount) {Mono.error(InsufficientStockException())} else {repo.save(inv.copy(quantity = inv.quantity - amount))// If another transaction modified this document,// @Version mismatch throws OptimisticLockingFailureException}}.retryWhen(Retry.backoff(3, Duration.ofMillis(100)).filter { it is OptimisticLockingFailureException })}}
CAP Theorem
Distributed stores guarantee at most two of: Consistency, Availability, Partition Tolerance. Reactive microservices typically choose AP with eventual consistency.
// โโโ CAP Theorem: choose 2 of 3 โโโ// Every distributed data store can guarantee at most TWO of://// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ// โ Consistency (C) โ// โ All nodes see same data โ// โ at the same time โ// โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ// โ// โโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโ// โ Availability (A) โ Partition// โ Every request gets a โ Tolerance (P)// โ response (success/fail) โ System works despite// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ network partitions//// CP: MongoDB, HBase, Redis Cluster โ consistent reads, may reject writes// AP: Cassandra, CouchDB, DynamoDB โ always available, eventually consistent// CA: Traditional RDBMS (single node) โ not partition tolerant// In reactive microservices, we usually choose AP (eventual consistency)// and use the Saga pattern for distributed transactions// Example: eventually consistent order status@Serviceclass OrderQueryService(private val repo: OrderRepository) {fun getStatus(orderId: String): Mono<OrderStatus> =repo.findById(orderId).map { it.status }.defaultIfEmpty(OrderStatus.UNKNOWN) // AP: always respond}
Messaging with Kafka
Kafka + Reactor for fully non-blocking event-driven pipelines. Messages consumed as Flux streams.

Kafka Pub-Sub architecture โ Producers โ Topics โ Kafka Broker โ Consumer Groups
// โโโ Reactive Kafka Consumer โโโ// Kafka messages consumed and exposed as a Flux stream@Serviceclass KafkaConsumerService {private val sink = Sinks.many().multicast().onBackpressureBuffer<String>()@KafkaListener(topics = ["orders-topic"], groupId = "webflux-group")fun consume(message: String) {sink.tryEmitNext(message)}fun getStream(): Flux<String> = sink.asFlux()}// โโโ SSE Controller streaming Kafka messages โโโ@RestControllerclass StreamController(private val kafka: KafkaConsumerService) {@GetMapping("/stream/orders", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])fun streamOrders(): Flux<String> =kafka.getStream().map { message -> "Order received: $message" }}// โโโ Kafka Producer (reactive) โโโ@Serviceclass KafkaProducerService(private val template: KafkaTemplate<String, String>) {fun publish(topic: String, message: String): Mono<Void> =Mono.fromFuture(template.send(topic, message).toCompletableFuture()).then()}
Publisher โ Broker (Kafka) โ Subscriber. Topic-based or content-based routing.
Saga Pattern
A sequence of local transactions with compensating actions โ the reactive alternative to 2PC for distributed transactions.
// โโโ Saga Pattern: distributed transactions via compensating actions โโโ// Instead of one big transaction, each service has a local transaction// + a compensating action if a downstream step fails@Serviceclass OrderSagaOrchestrator(private val orderService: OrderService,private val paymentService: PaymentService,private val inventoryService: InventoryService) {fun placeOrder(request: OrderRequest): Mono<OrderResult> {return orderService.create(request) // Step 1.flatMap { order ->paymentService.charge(order) // Step 2.flatMap { payment ->inventoryService.reserve(order) // Step 3.map { OrderResult.success(order, payment) }.onErrorResume { e ->// Step 3 failed โ compensate Step 2paymentService.refund(payment).then(orderService.cancel(order)).then(Mono.error(e))}}.onErrorResume { e ->// Step 2 failed โ compensate Step 1orderService.cancel(order).then(Mono.error(e))}}}}
Create Order โ Charge Payment โ Reserve Inventory โ Confirm. If Step 3 fails, compensations run in reverse. Services must be idempotent.
Resilience Patterns
Resilience4j Circuit Breaker, Bulkhead, and Retry โ preventing cascading failures in reactive systems.
// โโโ Circuit Breaker (Resilience4j) โโโ// Prevents cascading failures by "tripping" after too many errorsval circuitBreaker = CircuitBreaker.of("paymentService",CircuitBreakerConfig.custom().failureRateThreshold(50f) // trip at 50% failure rate.waitDurationInOpenState(Duration.ofSeconds(10)) // stay open 10s.slidingWindowSize(10) // evaluate last 10 calls.build())fun chargePayment(order: Order): Mono<Payment> {return circuitBreaker.run(paymentClient.charge(order), // primary call{ fallback -> Mono.just(Payment.pending()) } // fallback)}// โโโ Bulkhead (limit concurrent calls) โโโval bulkhead = Bulkhead.of("externalApi",BulkheadConfig.custom().maxConcurrentCalls(5) // max 5 simultaneous calls.maxWaitDuration(Duration.ofMillis(500)) // wait max 500ms for slot.build())// โโโ Retry with jitter โโโval retry = Retry.of("dbRetry",RetryConfig.custom<Any>().maxAttempts(3).waitDuration(Duration.ofMillis(500)).retryExceptions(TransientDataAccessException::class.java).build())
CLOSED (normal) โ OPEN (failures exceed threshold) โ HALF-OPEN (test recovery) โ CLOSED or back to OPEN.
Spring Cloud Gateway
Reactive API gateway built on WebFlux โ routing, rate limiting, circuit breaking, all non-blocking.
// โโโ Spring Cloud Gateway: reactive API gateway โโโ// Built on WebFlux โ fully non-blocking// application.yml configuration// spring:// cloud:// gateway:// routes:// - id: user-service// uri: lb://USER-SERVICE// predicates:// - Path=/users/**// filters:// - StripPrefix=1// Programmatic route definition (Kotlin DSL)@Configurationclass GatewayConfig {@Beanfun routes(builder: RouteLocatorBuilder): RouteLocator =builder.routes().route("user-service") { r ->r.path("/api/users/**").filters { f ->f.stripPrefix(1).addResponseHeader("X-Gateway", "Spring Cloud")}.uri("lb://USER-SERVICE")}.route("order-service") { r ->r.path("/api/orders/**").filters { f -> f.stripPrefix(1) }.uri("lb://ORDER-SERVICE")}.build()}// Custom Global Filter (logging, auth, metrics)@Componentclass AuthFilter : GlobalFilter, Ordered {override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {val token = exchange.request.headers.getFirst("Authorization")if (token == null || !token.startsWith("Bearer ")) {exchange.response.statusCode = HttpStatus.UNAUTHORIZEDreturn exchange.response.setComplete()}return chain.filter(exchange)}override fun getOrder(): Int = -1 // run first}
Gradle Dependencies
Here are the essential dependencies to get started with Spring Reactive in a Kotlin project:
// build.gradle.kts โ Spring Reactive dependenciesdependencies {implementation("org.springframework.boot:spring-boot-starter-webflux")implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")// Reactive data access (choose one or both)implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive")implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")runtimeOnly("io.r2dbc:r2dbc-postgresql")// TestingtestImplementation("io.projectreactor:reactor-test")}
Key Takeaways
- Mono (0..1) and Flux (0..N) are the reactive building blocks
- Backpressure via
request(n)prevents producers from overwhelming consumers - WebFlux runs on Netty event loop โ never
.block()on it - WebClient replaces RestTemplate โ use
Mono.zip()for parallel calls - Saga Pattern for distributed transactions, Resilience4j for fault tolerance
- Spring Cloud Gateway ties it all together as the reactive API entry point