Reactive Architecture at the Code Level

Until now, we have considered reactive architectures at the system level. The implementation of such systems is based on message or event exchanges. Queues are typically used as the medium for propagating events or messages, while simple message buses are less common.

We previously discussed the difference between a bus and a queue, and today we’ll talk about how reactive architecture is implemented at the code level. The core of implementing reactive architecture at the code level still revolves around messages, though the methods for message dissemination are not necessarily based on queues.

Reactive patterns used at the code level typically implement a "data stream" approach, allowing for modular or micromodular implementations. This approach works especially well in stateless systems where the order of data processing is not important, though it can also be applied in scenarios where order matters (e.g., using the SAGA pattern).

Today, we’ll focus on error-handling strategies at the code level, discuss what events and messages are, and dive into two specific patterns: Observer and Pub/Sub.

Error-Handling Strategy

The error-handling strategy is built on four assumptions that the application must handle:

  1. Software can cause errors.
  2. Hardware can cause errors.
  3. People can make mistakes.
  4. Timeout is an error.

The first three points are obvious—when handling errors, you need to ensure logical isolation of code that operates at different abstraction levels. This is where architectural boundaries and responsibility distribution come into play. These levels are often handled by the libraries and frameworks in use.

The fourth point defines a requirement for code design, which, when possible, should be controlled by tests.

The foundation of the strategy is "fault tolerance" rather than "failure prevention." This means that when designing code, we must consider an alternative "non-blocking" behavior path for failure situations. Null-object patterns, “empty events,” or transaction rollbacks can be actively used here.

The error-handling strategy at the code level should complement the overall system’s fault-tolerance strategy. This includes leveraging monitoring capabilities and redundancy (to ensure functionality in cases of partial node outages).

Design Considerations for Monitoring and Root Cause Analysis

  • Logging
  • Fail Fast

Software Considerations:

  • Processes: Support for multiple processes at the system or language level.
  • Green Threads: Lightweight threads.
  • EventLoop: Ability to implement asynchronous behavior within a single thread.
  • Promises: Deferred event handling.

Formal Models:

  • Process Calculi: A formal way to describe the interaction of parallel systems as a unified process.
  • Actor Model: A mathematical model of concurrent computation.

Depending on where the code is executed, one of two program behavior models can be used:

  1. Within a single process (event-driven).
  2. Distributed or inter-process (message-based).

If the program operates within a single process, its behavior can be seen as a set of events, each with an associated event handler. This way, the program can trigger an event handler when an event occurs (usually triggered by user actions or other event processing results).

Event Characteristics:

  • Format: Binary or textual.
  • Structure: Direct usage of RAM.
  • Behavior: Static or dynamic.
  • Handling: Via internal callback.

This approach is easily implemented within a modular monolith, allowing for the addition of reactive behavior to existing applications.

In distributed systems, interaction occurs through message exchange mechanisms, where "events" are transformed into corresponding messages.

Message Characteristics:

  • Format: Textual or binary.
  • Structure: With or without schema.
  • Behavior: Static or dynamic.
  • Handling: Delivered through propagation mediums (queues, buses, shared memory, etc.).
  • Ownership: Personal or shared (anonymous, broadcast, etc.).
  • Auxiliary Functions: Serialization, fragmentation, checksum, digital signatures.

Interaction through events and messages can be either synchronous (explicitly waiting for confirmation) or asynchronous. However, events always result in tight coupling.

Messages, on the other hand, are a universal way of organizing interaction between two elements, both at the code and system levels. There are three interaction models:

  1. Point-to-Point.
  2. Point-to-Stream.
  3. Stream-to-Point.

The "point-to-point" model creates strong coupling and hinders system scalability. Therefore, more interesting is the program’s operation based on data streams.

Main Elements of a Data Stream:

  • Messages / Serialized Events.
  • Message Types / Channels.

Main Transfer Mechanisms:

  • Buses.
  • Queues.

Reactive behavior is characterized by two system properties:

  1. Data Stream.
  2. Propagation of reaction across all dependent components.

With the rise of functional programming-based implementations of reactive architecture, a question arises: how can reactive architecture be implemented using traditional object-oriented patterns?

Let's consider three patterns that can be adapted to the reactive paradigm:

  • Pub/Sub
  • Observer

Pub/Sub and Observer

The Observer pattern cannot implement reactive behavior through a data stream, but it can be used in cases where reactivity is achieved within a system with "tight" coupling.

Observer

A common question is: what’s the difference between Pub/Sub and Observer? There are differences both in principle and in their implementation for reactive applications.

In the context of reactive architecture, it's important to note that both Pub/Sub and Observer work with event-driven architectures, but reactive behavior is achieved through "messages" that are extracted from the "data stream."

Key Differences:

  • The Observer "knows" its subscribers directly, which creates strong coupling.
  • The Observer immediately generates an "event," meaning it triggers a callback directly.
  • Pub/Sub separates "event publication" from its execution—the callback trigger.
  • Pub/Sub only "knows" about the topic and doesn’t know the actual subscribers.

Comments

Popular posts from this blog

Books Every Developer Should Read (In My Opinion)

TypeScript: Why It's Needed and Why It's So Popular