Reactive Systems
A reactive system is built to stay responsive under load and failure: it is responsive, resilient, elastic, and message-driven, as set out in the Reactive Manifesto. Note the distinction — reactive systems are an architectural goal, while reactive programming (observables, streams) is one tool that can help reach it. Don't confuse adopting Rx with building a system that actually stays up.
The Reactive Manifesto names four properties that reinforce each other. Responsive: the system replies in a timely way, even under stress. Resilient: it stays responsive when parts fail, through isolation and recovery. Elastic: it scales out and in with load. Message-driven: components communicate via asynchronous messages, which is what enables the other three — loose coupling, isolation, and back-pressure.
This is closely related to Event-Driven Architecture and Asynchronous Messaging (message-driven is the foundation) and to Distributed Systems & Consistency and Real-time & WebSockets. The goal isn't to chase a buzzword or rewrite everything as streams — it's to make the system degrade gracefully instead of falling over, which for a service handling money and compliance matters a great deal.
Build for the four properties
- DoTreat responsiveness as a contract: bound every operation with a timeout and have a defined behaviour when it is exceeded, so a slow dependency can't make you unresponsive (see Designing for Failure).
- DoIsolate failure with bulkheads and circuit breakers, so one struggling component or third party is contained rather than cascading into a full outage (see Third-Party Integrations).
- DoDesign for elasticity by keeping services stateless where possible and pushing shared state to backing stores, so you can scale horizontally without sticky sessions (see Multi-Tenancy, Concurrency).
- DoPrefer asynchronous, non-blocking message passing between components over synchronous chains of blocking calls; it is what lets you isolate, buffer, and scale.
- AlwaysHonour back-pressure: a fast producer must not overwhelm a slow consumer. Bound queues and signal upstream to slow down rather than accumulating unbounded work and running out of memory.
foreach (var item in hugeStream)
queue.Add(Process(item)); // no bound, no timeout
// consumer is slower than producer -> queue grows until OOM
// one slow downstream call blocks the whole thread
No back-pressure, no isolation, no timeout. Under load the queue grows without limit and a single slow dependency stalls everything.
var channel = Channel.CreateBounded- (1000); // back-pressure
// producer awaits when full; consumer pulls at its own rate
await policy.ExecuteAsync(ct => ProcessAsync(item, ct));
// policy = timeout + retry + circuit breaker (bulkhead)
The bounded channel applies back-pressure so producers slow down instead of exhausting memory; the resilience policy isolates and times out failures.
Use reactive programming as a tool, not a goal
- ConsiderReactive/streaming libraries (Rx, channels, IAsyncEnumerable) where you genuinely have asynchronous streams of events to compose, transform, and back-pressure.
- AvoidRewriting straightforward request/response code as observable pipelines for fashion. Reactive programming adds real cognitive cost and debugging difficulty; it must earn its place.
- DoRemember the distinction: you can build a responsive, resilient, elastic system with plain async/await, queues, and good failure handling — without any reactive-programming library at all.
Self-review checklist
- AskDoes every operation have a timeout and a defined behaviour when a dependency is slow or down?
- AskIf one component or third party fails, is it isolated — or does it take the whole system with it?
- AskIs there back-pressure, or can a fast producer exhaust memory against a slow consumer?
- AskAm I reaching for reactive programming because the problem needs it, or because it sounds modern?