Mono just() vs defer() vs create() in Reactive Programming

Tech Lead & Architect | 13+ Years in Cloud, Backend, and AI - Experienced software engineer with expertise in Java, Spring Boot, Microservices, Angular, React, Kafka, DevOps, Python, PySpark, Databricks, and Generative AI. Certified in TOGAF, AWS, and Google Cloud. Passionate about building scalable, secure, and high-performance systems. Enthusiast in Data Engineering & Agentic AI. Author of 1,200+ technical articles sharing insights across diverse tech stacks.
Date: 2025-01-15
Reactive Programming: Mastering Asynchronous Data Flows with Project Reactor's Mono
Reactive programming is a powerful paradigm shift in software development, enabling the creation of asynchronous, non-blocking, and event-driven applications. Instead of relying on traditional synchronous, request-response patterns, reactive systems react to streams of data, processing events as they arrive. This approach is particularly well-suited for handling concurrent operations and building highly scalable applications. Within the Project Reactor library, a popular Java framework for reactive programming, the Mono object plays a central role. A Mono represents a single asynchronous value—it either contains a single value or is empty. Understanding how to effectively utilize Mono is key to harnessing the power of reactive programming. This article will explore three fundamental methods for creating Mono objects: just(), defer(), and create(), explaining their distinct behaviors and ideal use cases.
The Mono.just() method provides a straightforward way to create a Mono that emits a predefined value. Imagine you have a known constant, such as a configuration setting or a pre-calculated result. Using just(), you can readily transform this value into a reactive stream. The beauty of this method lies in its simplicity and efficiency. The value is immediately available and emitted to any subscriber as soon as it subscribes to the Mono. There's no lazy evaluation or deferred computation; the value is prepared and ready for immediate consumption. This makes just() perfect for static or pre-computed values. However, its simplicity also represents a limitation: it's unsuitable for situations where the value needs to be generated dynamically or fetched from an external source upon subscription.
Consider a scenario where you need to display a welcome message on a webpage. The message itself is a fixed string, "Welcome to our site!". Using just(), you could create a Mono that emits this string, which a subscriber (perhaps a part of the webpage rendering engine) could then use to display the message. The subscriber's action of subscribing triggers the immediate emission of the "Welcome to our site!" message. This method's efficiency comes from the fact that the message is already ready; there's no need for any additional processing or fetching. But if the welcome message were to be personalized based on user data, just() would not be appropriate.
In contrast, Mono.defer() offers a mechanism for delaying the creation of the Mono until the moment a subscriber requests it. This method accepts a supplier function—a function that returns a Mono. Only when a subscriber subscribes to the Mono created by defer() does the supplier function execute, generating a fresh Mono instance. This deferred behavior is incredibly valuable when dealing with values that are generated dynamically or depend on external factors that might change over time. Imagine needing to fetch data from a database. Using defer(), you ensure that each subscriber receives the most up-to-date data. The database query is executed only when a subscriber is present, guaranteeing that stale data isn't inadvertently delivered.
Let's consider an example involving a user's profile information. Assume this information is stored in a database and can be updated frequently. If we were to use just(), we'd retrieve the profile data only once and the subsequent subscribers would receive that initial snapshot, regardless of any updates. However, using defer(), the database query is executed each time a subscriber requests the user's profile, guaranteeing that everyone receives the current information. This is crucial for applications that require real-time updates or where data consistency is paramount. The delay in creation inherently ensures freshness.
The Mono.create() method offers the most control, providing a way to programmatically emit a value or an error through a callback mechanism. This method grants complete authority over the emission process. The callback receives a MonoSink instance, allowing you to emit a single value, signal an error, or simply indicate completion without emitting any value. This level of control is particularly useful when integrating non-reactive code or asynchronous APIs into a reactive workflow. For instance, if you have an external library that uses callbacks or event listeners, create() provides a bridge to incorporate its asynchronous behavior into a reactive stream.
Suppose you're integrating with a legacy system that uses a callback-based API for retrieving data. This API triggers a callback upon successful data retrieval. You can use create() to wrap this callback-based API within a reactive flow. The callback function within create() would receive the data from the legacy system and use the MonoSink to emit that data as a Mono. This cleanly integrates the legacy system’s asynchronous nature into a reactive pipeline, maintaining a consistent reactive pattern throughout your application. The flexibility afforded by create() allows for handling a wide range of situations, but this power comes at the cost of increased complexity; it requires more careful design and implementation to handle error conditions correctly.
In summary, the choice between just(), defer(), and create() hinges on the nature of the data being emitted. just() excels with static values, offering simplicity and efficiency. defer() is ideal for dynamically generated or externally sourced values, guaranteeing freshness for each subscriber. And finally, create() empowers developers with maximum control, enabling seamless integration of non-reactive components into a reactive system. Understanding these distinctions is crucial for crafting efficient, responsive, and robust reactive applications using Project Reactor's Mono. By selecting the appropriate method, developers can optimize their reactive workflows, leading to more efficient and maintainable code.