DEV Community

Cover image for Effects Without Middleware — How Pipeline Stages Replace Thunks and Sagas
SDuX Vault
SDuX Vault

Posted on • Originally published at sdux-vault.com

Effects Without Middleware — How Pipeline Stages Replace Thunks and Sagas

Redux effects are middleware — thunks dispatching thunks, sagas yielding sagas, observables piping into more observables. Every async operation becomes a dispatch chain that resolves at arbitrary times with no ordering guarantee. SDuX Vault™ eliminates middleware entirely. Asynchronous inputs resolve through pipeline stages with serialized execution, deterministic ordering, and atomic state commitment.

The Middleware Problem

Redux was designed around synchronous reducer composition. When applications needed async operations — API calls, WebSocket messages, timer coordination — the core model had no answer. The community responded with middleware: thunks, sagas, and observables.

Each middleware layer intercepts dispatched actions and performs side effects before (or instead of) reaching the reducer. The result is a parallel execution system layered on top of the store:

// A thunk dispatches multiple actions at arbitrary times
function fetchUsers() {
  return async (dispatch) => {
    dispatch({ type: 'FETCH_USERS_START' });

    try {
      const response = await fetch('/api/users');
      const users = await response.json();
      dispatch({ type: 'FETCH_USERS_SUCCESS', payload: users });
    } catch (error) {
      dispatch({ type: 'FETCH_USERS_FAILURE', payload: error });
    }
  };
}

// Dispatch the thunk
dispatch(fetchUsers());
Enter fullscreen mode Exit fullscreen mode

This pattern introduces three separate dispatches for a single logical operation. Each dispatch broadcasts to the entire reducer tree. The timing of the success or failure dispatch depends on network latency — which means ordering relative to other operations is non-deterministic.

// A saga yields effects that resolve at framework-controlled times
function* fetchUsersSaga() {
  yield put({ type: 'FETCH_USERS_START' });

  try {
    const users = yield call(api.fetchUsers);
    yield put({ type: 'FETCH_USERS_SUCCESS', payload: users });
  } catch (error) {
    yield put({ type: 'FETCH_USERS_FAILURE', payload: error });
  }
}

// Root saga watches for trigger actions
function* rootSaga() {
  yield takeLatest('FETCH_USERS_REQUESTED', fetchUsersSaga);
}
Enter fullscreen mode Exit fullscreen mode

Sagas add a generator-based orchestration layer. They listen for actions, perform async work, and dispatch new actions. The complexity compounds: watchers, forks, races, channels — an entire concurrency framework layered on top of a state container.

⚠️ Warning: The fundamental issue is not the middleware libraries themselves. It is that Redux has no built-in model for async state resolution. Middleware exists because the core architecture cannot coordinate asynchronous inputs within its own execution model.

How Async Resolution Works in the SDuX Pipeline

SDuX Vault does not use middleware. Asynchronous input is handled through the Resolve stage — a core pipeline stage that normalizes all incoming inputs into a canonical value before downstream processing begins.

The Resolve stage accepts multiple input forms and resolves them under pipeline coordination:

Input Form Resolution
Plain state values Passed through immediately
Deferred factories (functions returning Promises) Invoked and awaited under pipeline control
Observable-based inputs Subscribed and resolved within pipeline lifecycle
Structured state envelopes Unwrapped and normalized
Angular HttpResourceRef (@sdux-vault/angular) Observed until a concrete value emits, then resolved

Regardless of how the input originates, the Resolve stage guarantees that downstream pipeline stages — operators, filters, reducers — always receive a predictable, normalized upstream value. No stage ever reasons about transport, timing, or source-specific concerns.

Key takeaway: The Resolve stage is a core pipeline stage. It is always present and executes automatically. You do not install or configure it. Every FeatureCell™ includes resolve behavior by default.

Resolve Behaviors vs Thunks

A Redux thunk dispatches new actions at arbitrary times. The store has no knowledge of when those actions will arrive or in what order. Multiple thunks executing concurrently can interleave their dispatches unpredictably.

In SDuX Vault, you submit a deferred factory directly to the owning FeatureCell. The pipeline resolves the async value under its own coordination — serialized through the conductor queue, committed atomically in a microtask boundary.

import { FeatureCell } from '@sdux-vault/core';

const userCell = FeatureCell('users', { value: [] });

// Submit a deferred factory — the pipeline resolves it
userCell.mergeState({
  value: () => fetch('/api/users').then((r) => r.json())
});
Enter fullscreen mode Exit fullscreen mode

Compare the two approaches side by side:

Concern Redux Thunk SDuX Resolve
Dispatch count per operation 3+ (start, success, failure) 1 (single mergeState call)
Ordering guarantee None — resolves at arbitrary times Serialized through conductor queue
Reentrancy risk Possible — dispatch during dispatch Structurally impossible
State commitment Immediate on each dispatch Deferred to microtask boundary
Middleware required Yes — redux-thunk No — built into the pipeline

The pipeline does not dispatch new actions after resolution. It resolves the input, processes it through operators, filters, and reducers, and commits the final state — all within a single pipeline execution. No secondary dispatch. No intermediate states visible to observers.

Controllers vs Saga Orchestration

Redux Sagas provide orchestration through generators — watching for actions, forking concurrent tasks, racing competing effects. The entire concurrency model lives outside the store in a parallel execution layer.

SDuX Vault provides orchestration through Controllers. Controllers govern execution authority for pipeline attempts — they evaluate whether an update is allowed, denied, or aborted before pipeline computation begins.

Concern Redux Saga SDuX Controller
Execution layer External middleware Pipeline Policy stage
Coordination model Generator-based (yield, fork, race) Policy evaluation before computation
Relationship to state Dispatches actions that reach reducers Does not touch data — governs execution authority only
Concurrency handling Manual (takeLatest, takeEvery, race) Serialized by conductor queue
Testability Generator stepping with mocked effects act → settle → assert with vaultSettled

Controllers do not dispatch, yield, or fork. They declare policy. The pipeline enforces that policy deterministically. This separation means your coordination logic never interleaves with your data transformation logic — they occupy different pipeline layers entirely.

Key takeaway: Controllers operate in the Policy Layer — the first stage of pipeline execution. They evaluate before any data processing occurs. Reducers operate in the Processing Layer, separated by multiple stage boundaries. The two concerns never mix.

The Execution Guarantee

Both thunks and sagas share a fundamental limitation: they dispatch actions at arbitrary times with no guarantee about ordering relative to other concurrent operations. Two thunks resolving simultaneously can interleave their dispatches. Two sagas forked in parallel can commit conflicting state.

SDuX Vault eliminates this entire category of bug through architectural constraints:

  • Every pipeline attempt is serialized through a FIFO conductor queue — one at a time, deterministic order
  • Pipeline computation is pure and side-effect free — no state mutation until computation completes
  • State commitment is deferred to a microtask boundary — observers never see partial results
  • Reentrancy is structurally impossible — no observer can trigger a new pipeline run while a commit is in progress

These are not conventions you must remember. They are architectural guarantees enforced by the runtime. You cannot accidentally bypass them.

Redux Comparison

Redux handles async through middleware that dispatches new actions at arbitrary times. SDuX Vault resolves async inputs through pipeline-coordinated stages with serialized execution.

Dimension Redux (Thunk/Saga/Observable) SDuX
Async model External middleware layer Built-in Resolve stage
Coordination Manual (takeLatest, debounce, race) Pipeline-managed serialization
Side effect scope Anywhere in middleware chain Contained within pipeline lifecycle
Ordering guarantee None without manual effort FIFO queue ensures deterministic order
State visibility Intermediate states visible between dispatches Only final atomic snapshots are observable
Dependencies redux-thunk, redux-saga, or redux-observable None — resolve is a core pipeline stage

Try It Yourself

Explore how SDuX Vault handles async state resolution without middleware:

Top comments (0)