Skip to content

RP Reconsiderada - Design vs Realidade ​

Reactive Programming (RP) is widely known as a powerful paradigm for asynchronous data stream processing.

But is RP really a panacea? This page examines the gap between RP ideals and reality, and objectively considers where RP should and should not be used.

RP ideals vs. reality ​

Ideal: Sophisticated modern design ​

RPs are often advertised as follows

  • Declarative and easy-to-read code
  • Concise expression of asynchronous processing
  • handles complex data flows in a unified manner
  • core technology of reactive architecture.

Reality: It can reduce team productivity. ​

However, the following problems have been encountered in actual projects

  • very high learning curve
  • Difficult to debug
  • Complex testing
  • Loss of productivity due to misuse

WARNING

Applying RP to "all code" may conversely increase code complexity and reduce team productivity.

Four challenges facing RP ​

1. Height of learning curve ​

Mastering RP requires a different thought model than traditional imperative programming.

Difficult to track data flow ​

typescript
// ❌ Data flow is difficult to see
source$
  .pipe(
    mergeMap(x => fetchData(x)),
    switchMap(data => processData(data)),
    concatMap(result => saveData(result))
  )
  .subscribe(/*...*/);

problem

  • Differences between mergeMap, switchMap, and concatMap are not intuitive
  • Difficult to track where and how data is transformed
  • Difficult to identify where the error occurred

Debugging and logging difficulties ​

typescript
// Difficult to debug
source$
  .pipe(
    map(x => x * 2),
    filter(x => x > 10),
    mergeMap(x => api(x))
  )
  .subscribe(/*...*/);

// Where did the error occur?
// Which operator lost the value?

TIP

The tap() operator is used for debugging, which itself is an additional learning cost.

typescript
source$
  .pipe(
    tap(x => console.log('Before map:', x)),
    map(x => x * 2),
    tap(x => console.log('After map:', x)),
    filter(x => x > 10),
    tap(x => console.log('After filter:', x))
  )
  .subscribe(/*...*/);

2. High cognitive load ​

There are more than 100 operators in the RP, and their use is complex.

Too many choices of operators. ​

criterionoptionsdifference
Sequential processing of arraysconcatMap, mergeMap, switchMap, exhaustMapConcurrency and order guarantees differ
Combining Multiple Streamsconcat, merge, combineLatest, zip, forkJoin, raceDifferent bonding methods
error handlingcatchError, retry, retryWhen, onErrorResumeNextRetry strategies differ

Why bother writing a process in RP that can be done with a simple if or await?

typescript
// ❌ Example of complex writing in RP
of(user)
  .pipe(
    mergeMap(u => u.isPremium
      ? fetchPremiumData(u)
      : fetchBasicData(u)
    )
  )
  .subscribe(/*...*/);

// ✅ Simple conditional branching
const data = user.isPremium
  ? await fetchPremiumData(user)
  : await fetchBasicData(user);

3. Difficulty of the test ​

Testing RP requires an understanding of time control and Marble Testing.

Marble Testing's Learning Cost ​

typescript
import { TestScheduler } from 'rxjs/testing';

it('debounceTime test', () => {
  const testScheduler = new TestScheduler((actual, expected) => {
    expect(actual).toEqual(expected);
  });

  testScheduler.run(({ cold, expectObservable }) => {
    const input$  = cold('-a-b-c---|');
    const expected =     '-----c---|';

    const result$ = input$.pipe(debounceTime(50, testScheduler));

    expectObservable(result$).toBe(expected);
  });
});

problem

  • Need to learn Marble Diagram notation
  • Need to understand time control mechanism
  • Higher learning cost than normal unit testing

Frequent synchronization bugs ​

typescript
// ❌ Common bugs: subscription timing issues
const subject$ = new Subject();

subject$.next(1);  // this value is not accepted
subject$.subscribe(x => console.log(x));  // subscription is slow
subject$.next(2);  // I can take this.

4. Complications due to misuse ​

Applying RP to all codes creates unnecessary complexity.

Over-application to simple CRUD processes ​

typescript
// ❌ Excessive RP application
getUserById(userId: string): Observable<User> {
  return this.http.get<User>(`/api/users/${userId}`)
    .pipe(
      map(user => this.transformUser(user)),
      catchError(error => {
        console.error('Error:', error);
        return throwError(() => error);
      })
    );
}

// ✅ Simple Promise
async getUserById(userId: string): Promise<User> {
  try {
    const user = await fetch(`/api/users/${userId}`).then(r => r.json());
    return this.transformUser(user);
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

IMPORTANT

RP is not a "silver bullet that solves all problems." It is important to identify areas to apply and areas to avoid.

Areas where RP excels ​

RP is not a panacea, but it is very powerful in the following areas

1. Continuous data stream processing ​

Ideal for processing continuously occurring data such as sensor data, log streams, real-time data, etc.

typescript
// ✅ Example of where RP has an advantage: processing sensor data
sensorStream$
  .pipe(
    filter(reading => reading.value > threshold),
    bufferTime(1000),                           // Aggregate every second
    map(readings => calculateAverage(readings)),
    distinctUntilChanged()                      // Notify only when there is a change
  )
  .subscribe(avg => updateDashboard(avg));

2. WebSockets and push notifications ​

Ideal for bidirectional communication and push-type data delivery from servers.

typescript
// ✅ Reactive processing of WebSocket communication
const socket$ = webSocket('wss://example.com/socket');

socket$
  .pipe(
    retry({ count: 3, delay: 1000 }),  // Auto-reconnect
    map(msg => parseMessage(msg)),
    filter(msg => msg.type === 'notification')
  )
  .subscribe(notification => showNotification(notification));

3. state management system ​

NgRx, Redux Observable, MobX, etc., are effective as a foundation for state management libraries.

typescript
// ✅ RP utilization in state management (NgRx Effects)
loadUsers$ = createEffect(() =>
  this.actions$.pipe(
    ofType(UserActions.loadUsers),
    mergeMap(() =>
      this.userService.getUsers().pipe(
        map(users => UserActions.loadUsersSuccess({ users })),
        catchError(error => of(UserActions.loadUsersFailure({ error })))
      )
    )
  )
);

4. Back-end non-blocking I/O ​

Suitable for asynchronous backend processing such as Node.js Streams, Spring WebFlux, Vert.x, etc.

typescript
// ✅ RP-like processing of Node.js Streams
const fileStream = fs.createReadStream('large-file.txt');
const transformStream = new Transform({
  transform(chunk, encoding, callback) {
    const processed = processChunk(chunk);
    callback(null, processed);
  }
});

fileStream.pipe(transformStream).pipe(outputStream);

5. event driven distributed system ​

Kafka, RabbitMQ, Akka Streams, etc. are effective as the foundation for event-driven architectures.

Areas where RP is unsuitable ​

In the following areas, the code is simpler and more maintainable without RP.

1. Simple CRUD processing ​

For simple read/write operations to the database, async/await is more suitable.

typescript
// ❌ No need to write in RP
getUser(id: string): Observable<User> {
  return this.http.get<User>(`/api/users/${id}`);
}

// ✅ async/await is sufficient
async getUser(id: string): Promise<User> {
  return await fetch(`/api/users/${id}`).then(r => r.json());
}

2. Simple conditional branching ​

There is no need to stream a process that can be done with a simple if statement.

typescript
// ❌ Excessive RP application
of(value)
  .pipe(
    mergeMap(v => v > 10 ? doA(v) : doB(v))
  )
  .subscribe();

// ✅ Simple conditional branching
if (value > 10) {
  doA(value);
} else {
  doB(value);
}

3. One-time asynchronous processing ​

If Promise is sufficient, there is no need to make it Observable.

typescript
// ❌ Unnecessary Observable
from(fetchData()).subscribe(data => process(data));

// ✅ Promise is good enough
fetchData().then(data => process(data));

Evolution of RP: Toward simpler abstraction ​

The philosophy of RP is not disappearing, but is evolving into a simpler, more transparent form.

Angular Signals (Angular 19+) ​

typescript
// Signal-based reactivity
const count = signal(0);
const doubled = computed(() => count() * 2);

effect(() => {
  console.log('Count:', count());
});

count.set(5);  // Simple and intuitive

Features:

  • Lower learning cost than RxJS
  • Easy to debug
  • Fine-grained reactivity

React Concurrent Features ​

typescript
// React 18's Concurrent Rendering
function UserProfile({ userId }) {
  const user = use(fetchUser(userId));  // Integrated with Suspense
  return <div>{user.name}</div>;
}

Features:

  • Declarative data fetching
  • Automatic priority control
  • Hides RP complexity

Svelte 5 Runes ​

typescript
// Svelte 5's Runes ($state, $derived)
let count = $state(0);
let doubled = $derived(count * 2);

function increment() {
  count++;  // Intuitive updates
}

Features:

  • Optimization by compiler
  • No boilerplate
  • Transparency of reactivity

TIP

These new abstractions significantly reduce complexity while retaining the RP's core value (reactivity).

Policy for appropriate use of RP ​

1. Determine problem areas ​

Suitable for.Not suitable for
continuous data streamSimple CRUD
WebSocket CommunicationOne-time API call
Multiple asynchronous event integrationSimple conditional branching
real-time data processingStatic data conversion
state managementSimple variable update

2. Phased introduction ​

typescript
// ❌ Don't introduce all of them out of the blue
class UserService {
  getUser$ = (id: string) => this.http.get<User>(`/api/users/${id}`);
  updateUser$ = (user: User) => this.http.put<User>(`/api/users/${user.id}`, user);
  deleteUser$ = (id: string) => this.http.delete(`/api/users/${id}`);
  // Make everything Observable
}

// ✅ RP only where necessary
class UserService {
  async getUser(id: string): Promise<User> { /* ... */ }
  async updateUser(user: User): Promise<User> { /* ... */ }

  // Observable only where real-time updates are required
  watchUser(id: string): Observable<User> {
    return this.websocket.watch(`/users/${id}`);
  }
}

3. Consider the team's proficiency level ​

Team StatusRecommended Approach
Unfamiliar with RPLimited implementation (only where there are clear benefits such as WebSockets)
Partially masteredGradual expansion (state management, real-time processing)
All are proficientFull stack utilization (front-end to back-end)

4. Compare with alternatives ​

typescript
// Pattern 1: RP (when multiple events need to be integrated)
combineLatest([
  formValue$,
  validation$,
  apiStatus$
]).pipe(
  map(([value, isValid, status]) => ({
    canSubmit: isValid && status === 'ready',
    value
  }))
);

// Pattern 2: Signals (simpler reactivity)
const formValue = signal({});
const validation = signal(false);
const apiStatus = signal('ready');
const canSubmit = computed(() =>
  validation() && apiStatus() === 'ready'
);

// Pattern 3: async/await (one-time processing)
async function submitForm() {
  const isValid = await validateForm(formValue);
  if (!isValid) return;

  const result = await submitToApi(formValue);
  return result;
}

Summary ​

RP is not a panacea ​

IMPORTANT

Reactive Programming is neither harmful nor a panacea. It is a specialized tool optimized for asynchronous and event flow problems.

Appreciate the value of RP, but understand its limitations. ​

Areas where RP excels

  • Continuous data stream processing
  • WebSocket and real-time communication
  • State management systems
  • Back-end non-blocking I/O
  • Event-driven distributed systems

Areas where RP is unsuitable

  • Simple CRUD processing
  • Simple conditional branching
  • One-time asynchronous processing

Moving to a new abstraction ​

The philosophy of RP is evolving into simpler and more transparent forms such as Angular Signals, React Concurrent Features, and Svelte Runes.

Guidelines for Application in Practice ​

1.Identify problem areas - Is RP really necessary? 2.Implement in phases - don't just adopt it out of the blue 3.Consider team proficiency - learning costs are high 4.Compare with alternatives - are async/await and Signals sufficient?

TIP

"Use the right tools, in the right place." This is the key to a successful RP.

References ​

Lançado sob a licença CC-BY-4.0.