Sequential, sequential. I used to be that way too—living peacefully among my loops, my lists, my collections. If I wanted to transform a list: for; if I wanted to filter: for. Everything was predictable.
Until one day someone told me: “you can do that with RxJava”—and that’s when my journey began through lambdas, subscribe, and a new way to compose logic that’s clean, fluid, and powerful.
You don’t need to be a guru to get started—just be ready to adapt and open your mind to a new paradigm. In the following post, we’ll walk through the first steps to make that leap, covering the core ideas of reactive programming before diving into subscribe, error handling, and key operations like map, flatMap, and more.
Why Make the Switch: Real Motivations
1 The Limits of Sequential Thinking
Sequential programming always works... until it doesn’t. When you face massive data processing, concurrent calls to multiple external services, or need to handle errors at scale, what seemed like simple logic can turn into a cathedral arch—or a massive labyrinth of nested structures.
What starts as a for loop inside another for loop, quickly evolves—especially as your integrations grow—into a tangled mess of ifs, try-catch blocks, and status checks that complicate your clean code.
Suddenly you need to consult three different services (say, users, orders, and inventory), and you're doing it one after another:
try {
final Usuario usuario = getUsuario(id);
try {
final List<Pedido> pedidos = getPedidos(usuario);
try {
final Inventario inv = getInventario(pedidos);
} catch (final InventarioException e) {
log.error("Error en inventario", e);
}
} catch (final PedidoException e) {
log.error("Error en pedidos", e);
}
} catch (final UsuarioException e) {
log.error("Error en usuario", e);
}
We end up adding complexity. What was supposed to be a simple query becomes a block of nested try-catch statements, which can lead to duplicated logic, testing difficulties... not to mention our dear old friend: the NullPointerException. We try to force a step-by-step model in a world full of events, flows, and ever-changing conditions.
2 Think in Streams, Not Steps
RxJava introduces a new way of thinking and reasoning to solve our problems: it treats data as streams. This means we need to transform, filter, and handle them declaratively. That is, I don't think about what comes first or next, but rather “what do I do and how do I react when the moment comes?”
3 When to Use RxJava
RxJava is highly recommended in a variety of contexts:
- Parallel REST calls where you need concurrency.
- Android applications that need to manage multiple background threads.
- Retrying multiple API calls using
.retryWhen()
Why RxJava?
- Very mature and widely adopted in major projects like Netflix (always a tech pioneer, as this post explains) or SoundCloud (they’ve been using it since 2013!).
- Easy to integrate with popular frameworks, such as Spring.
- Ideal for getting started in the reactive world before diving into bigger frameworks like Reactor, WebFlux, or even Kafka Streams.
4 Key Features
RxJava and reactive programming in general are based on these principles:
- Asynchrony: no thread blocking, enabling more efficient resource usage.
- Backpressure: a mechanism to control how data is emitted when the consumer can’t keep up with the producer.
- Streams: as we’ve mentioned, data is treated as sequences.
- Immutability: data isn’t modified directly in the stream. Instead, it’s transformed, generating new values.

Getting Started with RxJava
Coming from a fully imperative world, the key—as in almost all programming—is to start small.
Creating Streams
Observable saludo = Observable.just("Hola", "desde", "RxJava"); saludo.subscribe(System.out::println);
Essential Operators
Observable.just("Java", "Kotlin", "Scala")
.filter(lang -> lang.length() > 4)
.map(String::toUpperCase)
.subscribe(System.out::println);
We have an example with something closer to our day-to-day work:
Observable.just(
new Usuario("Ana", true, "ana@email.com"),
new Usuario("Luis", false, "luis@email.com"),
new Usuario("Marta", true, "marta@email.com")
)
.filter(Usuario::isActivo)
.map(usuario -> new ResumenUsuario(usuario.getNombre(), usuario.getEmail()))
.subscribe(this::enviarResumen);
class ResumenUsuario {
private String nombre;
private String email;
public ResumenUsuario(String nombre, String email) {
this.nombre = nombre;
this.email = email;
}
// getters, toString, etc.
}
void enviarResumen(ResumenUsuario resumen) {
//Imaginemos una llamada externa
clienteAuditoria.enviar(resumen);
}
Just like in imperative programming, lambdas will still be our friends, as well as the main operators: map, filter, and flatMap. These are just a few from a loooooong list, as you can see in the official RxJava documentation.

In fact, flatMap helps us transform nested or asynchronous streams, making it one of the main pillars of reactive programming:
Observable.just("123", "456")
.flatMap(this::buscarUsuarioPorId)
.subscribe(System.out::println);
Observable<Usuario> buscarUsuarioPorId(String id) {
return Observable.fromCallable(() -> {
return new Usuario(id, "Nombre" + id);
});
}

Thread control
With the operations provided by the RxJava Scheduler library, we can manage thread control.
observable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(...);
We use subscribeOn to choose the thread where the subscription happens and observeOn to choose the thread where the results are observed.
Error handling
As we've mentioned, error handling is also simplified, but it’s essential (and you’ll see why when you need to debug) to properly manage your threads and react to errors.
Observable.just("http://api")
.flatMap(this::llamadaHttp)
.retry(3)
.onErrorReturnItem("valor por defecto")
.subscribe(System.out::println);
This way, no more endless try-catch blocks. The retry operator tells RxJava how many times it should retry in case of failure, while onErrorReturnItem acts as a fallback to emit a value if all retries fail. This is very useful to prevent the stream from breaking entirely and to handle scenarios where the system can continue using a default or known value, for example:
Observable.just(new Usuario("Juan", "juan@email.com"))
.flatMap(this::crearUsuario)
.retry(3)
.onErrorReturnItem(new Usuario("usuario_desconocido", "no-email@dominio.com"))
.subscribe(usuario -> guardarEnCache(usuario));
// Simulación de llamada a un servicio que crea un usuario
Observable<Usuario> crearUsuario(Usuario usuario) {
return apiClient.crearUsuario(usuario); }
void guardarEnCache(Usuario usuario) {
cacheRepository.save(usuario);
}
To highlight the difference between reactive and sequential programming, here’s how that same example would look using a sequential approach:
public void createUserSequential() {
final Usuario usuario = new Usuario("Juan", "juan@email.com");
final Usuario usuarioCreado = intentarCrearUsuarioConReintentos(usuario, 3).orElseGet(() -> new Usuario("usuario_desconocido", "no-email@dominio.com"));
guardarEnCache(usuarioCreado);
}
private Optional<Usuario> createUserWithRetry(Usuario usuario, int intentosMax) {
return IntStream.range(0, intentosMax)
.mapToObj(i -> {
try {
return Optional.of(apiClient.crearUsuario(usuario));
} catch (Exception e) {
return Optional.<Usuario>empty();
}
})
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
}
void guardarEnCache(Usuario usuario) {
cacheRepository.save(usuario);
}
Error handling could easily be a post of its own, so if you want to dive deeper, here’s a good starting point: Basic RxJava error handling.
We need to keep all of this in mind with the following mindset: instead of executing steps, we define a flow. Instead of managing all the state, we react to data.
How to Avoid Common RxJava Mistakes
Since I stumbled quite a bit in my early days with RxJava, here are a few tips to help you avoid those pitfalls (or at least spot them in time!).
Using subscribe() to "do important stuff"
Avoid putting business logic inside subscribe(). That smells like imperative programming. Ideally, all your logic should live inside the pipeline, leaving subscribe() solely for the final consumption.
Observable.just(1, 2, 3)
.map(x -> x * 2)
.subscribe(this::guardarEnBaseDeDatos);
Mixing Flows and Shared State
A core principle of reactive programming is immutability. So, if we find ourselves needing something mutable, it's a clear sign that we should rethink the approach, as there's likely a better way to express it reactively.
List<String> resultados = new ArrayList<>();
observable
.map(this::transformar)
.subscribe(resultados::add);
Choosing RxJava Without a Reason
As with everything in the world of Software Engineering, we shouldn’t pick a solution just because it’s trendy. RxJava is not the hammer for every nail. If your app doesn’t require asynchrony, don’t use RxJava. Use RxJava only when reactivity makes your life easier — not when it makes it harder.
Conclusion
The leap from sequential to reactive programming with RxJava may seem overwhelming at first, but with time and dedication, it will give us a new mindset and a better way to tackle some challenges in our work. We’ll gain clearer thinking, better expressiveness, and more power to handle complex data flows.
As my mom used to say: “Don’t bite off more than you can chew,” so don’t try to master everything at once — experiment, fail, and learn. RxJava will go from being “that weird new thing” to your best friend in many of your future architectures. Don’t just try to write reactive code — think reactively.
References
Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.
Tell us what you think.