RxJava from the Inside: Architecture Overview and How to Navigate 877 Source Files
Prerequisites
- ›Basic familiarity with reactive programming concepts (Observable, subscribe, operators)
- ›Java module system basics (module-info.java)
RxJava from the Inside: Architecture Overview and How to Navigate 877 Source Files
RxJava is one of those libraries where the public API feels effortless — chain a few operators, subscribe, done. But behind that fluency sits a codebase of extraordinary density: hundreds of operator implementations, lock-free queues, three distinct subscription protocols, and a scheduler system that now spans platform threads and virtual threads alike. This series cracks it open. We start here, with the map.
Project Identity and build tool
RxJava 4.x is a single-module Gradle project targeting Java 26 as its baseline. This is a significant jump from RxJava 3.x, which supported Java 8. The upgrade unlocks virtual threads, sealed types, records, and pattern matching — all of which the 4.x codebase uses.
The build configuration lives in build.gradle#L64-L70:
java {
toolchain {
languageVersion = JavaLanguageVersion.of(toolchainJdk)
}
sourceCompatibility = JavaVersion.VERSION_26
targetCompatibility = JavaVersion.VERSION_26
}
One detail worth noting: RxJava 4.x has zero runtime dependencies. The external org.reactivestreams library is gone — Flowable now implements java.util.concurrent.Flow.Publisher directly, which ships with the JDK. The only external dependencies are test-scoped: JUnit, TestNG, Mockito, Guava, the Reactive Streams TCK, and JMH.
The project identity is defined in gradle.properties:
group=io.reactivex.rxjava4
version=4.0.0-SNAPSHOT
POM_ARTIFACT_ID=rxjava
Tip: The
rx4.*prefix on system properties (likerx4.buffer-sizeandrx4.computation-threads) is the namespace for RxJava 4.x, distinct fromrx3.*in 3.x. This means you can run both versions side by side without property collisions.
Directory Structure Map
The source tree follows a deeply-nested but predictable layout. Here's the key package breakdown:
| Package | Purpose | Approximate File Count |
|---|---|---|
core/ |
The 5+2 reactive types, Scheduler, annotations | ~20 |
core/docs/ |
Sealed interfaces for Javadoc splitting | ~2 |
core/config/ |
Configuration records (e.g., FlatMapConfig) | ~1 |
internal/operators/flowable/ |
Flowable operator implementations | ~180 |
internal/operators/observable/ |
Observable operator implementations | ~150 |
internal/operators/single/ |
Single operator implementations | ~50 |
internal/operators/maybe/ |
Maybe operator implementations | ~50 |
internal/operators/completable/ |
Completable operator implementations | ~45 |
internal/operators/mixed/ |
Cross-type operators (e.g., Flowable→Single) | ~15 |
internal/operators/streamable/ |
Streamable operator implementations | ~5 |
internal/schedulers/ |
Scheduler implementations | ~15 |
internal/subscribers/ |
Base subscriber classes for operator internals | ~10 |
internal/subscriptions/ |
Subscription helpers | ~5 |
internal/util/ |
BackpressureHelper, AtomicThrowable, etc. | ~10 |
internal/virtual/ |
Virtual thread bridge implementations | ~3 |
schedulers/ |
Public scheduler API (Schedulers, TestScheduler) | ~5 |
plugins/ |
RxJavaPlugins global hooks | 1 |
operators/ |
Public queue and fusion interfaces | ~8 |
subjects/, processors/ |
Hot sources | ~10 |
parallel/ |
ParallelFlowable | ~25 |
The five core reactive type files are massive. Flowable.java alone is over 21,000 lines. These files define the entire fluent API surface — every map(), filter(), flatMap() you've ever called is a method on one of these classes. But the implementation of each operator lives in a separate class under internal/operators/.
The Public/Internal API Boundary
RxJava enforces a hard API boundary through the Java module system. The module definition at src/main/module/module-info.java#L14-L34 is explicit about what's exported:
module io.reactivex.rxjava4 {
exports io.reactivex.rxjava4.annotations;
exports io.reactivex.rxjava4.core;
exports io.reactivex.rxjava4.core.docs;
exports io.reactivex.rxjava4.core.config;
exports io.reactivex.rxjava4.disposables;
exports io.reactivex.rxjava4.exceptions;
// ... 10 more exported packages
requires java.management;
}
Notice what's not listed: anything under io.reactivex.rxjava4.internal. That's where the vast majority of code lives — all operator implementations, queue data structures, scheduler internals, and utility classes. Consumers of the library simply cannot access these types when running on the module path.
flowchart TD
subgraph Public API ["Public API (exported)"]
A[core/ - Flowable, Observable, etc.]
B[schedulers/ - Schedulers, TestScheduler]
C[plugins/ - RxJavaPlugins]
D[operators/ - QueueFuseable, SpscArrayQueue]
E[disposables/ - Disposable]
end
subgraph Internal ["Internal (not exported)"]
F[internal/operators/ ~500 files]
G[internal/schedulers/]
H[internal/subscribers/]
I[internal/util/]
J[internal/virtual/]
end
A -->|"delegates to"| F
B -->|"creates"| G
A -->|"uses"| H
F -->|"uses"| I
This design means the public API surface is remarkably small compared to the implementation. The five core classes act as facades — their methods are thin delegation wrappers that construct internal operator objects.
The 5+2 Core Reactive Types
RxJava defines five primary reactive types and two specialized ones. Each serves a distinct use case:
classDiagram
class Flowable~T~ {
<<non-sealed>>
+subscribe(Subscriber)
+subscribeActual(Subscriber)*
Backpressure: Yes
Items: 0..N
}
class Observable~T~ {
<<abstract>>
+subscribe(Observer)
+subscribeActual(Observer)*
Backpressure: No
Items: 0..N
}
class Single~T~ {
<<abstract>>
+subscribe(SingleObserver)
Items: exactly 1
}
class Maybe~T~ {
<<abstract>>
+subscribe(MaybeObserver)
Items: 0 or 1
}
class Completable {
<<abstract>>
+subscribe(CompletableObserver)
Items: 0
}
class Streamable~T~ {
<<interface>>
+stream(): Streamer~T~
Pull-based async enumerable
}
class ParallelFlowable~T~ {
<<abstract>>
+subscribe(Subscriber[])
Splits across rails
}
Flowable is the workhorse — backpressure-aware, implements Flow.Publisher, and is the right choice for any stream where the producer might outpace the consumer. Observable is its lighter sibling: no backpressure overhead, ideal for UI events and other inherently bounded sources.
Single, Maybe, and Completable are cardinality-restricted types. They exist because a stream that emits exactly one value (like an HTTP response) has fundamentally different semantics than an unbounded stream. These types encode that constraint in the type system.
The two newcomers in 4.x are Streamable (a pull-based async enumerable designed for virtual threads) and ParallelFlowable (which splits a stream across parallel "rails" for CPU-bound work). We'll cover Streamable in depth in Part 6.
Tip: When choosing a type, think cardinality first: 0 items →
Completable, 0-or-1 →Maybe, exactly 1 →Single, 0..N →FlowableorObservable. Then decide on backpressure: network/IO sources →Flowable, UI events →Observable.
Navigating Operators: The Naming Convention
With 500+ operator files, you need a system to find things. RxJava provides one through a rigid naming convention:
Pattern: {ReactiveType}{OperatorName}.java in internal/operators/{type}/
Flowable.map()→FlowableMap.javaininternal/operators/flowable/Observable.map()→ObservableMap.javaininternal/operators/observable/Flowable.flatMap()→FlowableFlatMap.javaSingle.map()→SingleMap.java
This convention is enforced by the validator meta-tests we'll examine in Part 7. The InternalWrongNaming validator catches violations automatically.
Some operators have suffixed variants for cross-type conversions or overloads:
FlowableCollectSingle.java— a Flowable operator that returns a SingleFlowableElementAtMaybe.java— returns a MaybeFlowableConcatMapScheduler.java— scheduler-parameterized variant
Once you internalize this pattern, you can navigate from any API call to its implementation in seconds.
Configuration: System Properties and RxJavaPlugins
RxJava doesn't use configuration files. Instead, it offers two configuration mechanisms: system properties and the plugin hook system.
System properties (set before class loading) control buffer sizes and thread pools. All use the rx4. prefix:
| Property | Default | Effect |
|---|---|---|
rx4.buffer-size |
128 | Default operator buffer size |
rx4.computation-threads |
CPU count | Computation scheduler pool size |
rx4.cached-keep-alive-time |
60 (seconds) | IO scheduler worker expiry |
rx4.computation-priority |
NORM_PRIORITY | Computation thread priority |
rx4.scheduler.use-nanotime |
false | Use nanoTime instead of currentTimeMillis |
RxJavaPlugins provides runtime hooks for error handling, scheduler replacement, and instrumentation. The hook fields are defined in RxJavaPlugins.java#L34-L135:
flowchart LR
subgraph RxJavaPlugins
EH[errorHandler]
SA[onFlowableAssembly]
SS[onFlowableSubscribe]
SCH[onScheduleHandler]
SI[onInitComputationHandler]
LD[lockdown]
end
SA -->|"intercepts"| OP[Operator creation]
SS -->|"intercepts"| SUB[subscribe call]
EH -->|"catches"| UE[Undeliverable errors]
SCH -->|"wraps"| RUN[Scheduled Runnables]
LD -->|"freezes"| SA
LD -->|"freezes"| SS
The lockdown() method is particularly important for production environments — once called, no plugin hooks can be changed, preventing libraries from interfering with each other's configuration.
What's Next
With the map in hand, we're ready to dive into the machinery. In Part 2, we'll trace the complete subscribe chain — from when you call flowable.subscribe(subscriber) through the plugin hooks, down through the abstract subscribeActual(), and into the operator decorator pattern that makes the entire fluent API possible. We'll use FlowableMap as our canonical example of how every operator in RxJava is built.