Next.js Architecture

Building a Resilient Next.js Storefront with Circuit Breaker and Magento PaaS

A practical architecture for protecting a modern Next.js storefront from slow or unavailable commerce services while keeping Magento PaaS as the core commerce backend.

Next.js App Router TypeScript Magento PaaS Circuit Breaker Resilience Pattern

Next.js in Front. Magento PaaS Behind It.

A modern commerce storefront does not need to expose the ecommerce platform directly to every browser interaction.

A cleaner architecture is to let Next.js own the customer-facing experience while Magento or Adobe Commerce PaaS remains responsible for the actual commerce domain: products, pricing, inventory, carts, customers and orders.

In this model, Next.js is more than a React frontend. Its server-side capabilities can operate as a lightweight Backend for Frontend, or BFF.

Customer Browser
Product pages, category pages, cart and checkout experience.
Next.js Application
App Router, Server Components, Route Handlers, caching, orchestration and resilience logic.
Adobe Commerce
Commerce APIs, catalog, customer, pricing, inventory, cart and order management.
Commerce Ecosystem
ERP, OMS, tax, payment, shipping and other external services.

The interesting question is what happens when the Magento API, or another dependency behind Magento, becomes temporarily slow.

If Next.js blindly forwards every request to an unhealthy upstream service, the storefront can become slow even though the Next.js application itself is perfectly healthy.

The Real Problem Is Not Failure. It Is Repeated Failure.

Assume a product detail page requests pricing and availability through Magento.

During normal operation the request may complete in a few hundred milliseconds.

Now imagine Magento, an upstream integration, or the network path becomes unhealthy. A request that normally takes 300ms may begin waiting several seconds before timing out.

Customer Request
       |
       v
   Next.js
       |
       v
 Magento API
       |
       X  timeout

Request 2  -> same timeout
Request 3  -> same timeout
Request 4  -> same timeout
Request 5  -> same timeout

The important signal here is that after several failures, the application has already learned something: the dependency is probably unhealthy.

Continuing to call it for every request provides little value. It only increases latency and generates additional load.

Architecture principle

An upstream outage should not automatically become a storefront outage. Next.js can create a resilience boundary between the user experience and the commerce backend.

The Circuit Breaker Model

The Circuit Breaker pattern tracks upstream failures and decides whether another request should be allowed.

CLOSED

Magento calls operate normally. Failures are counted.

OPEN

Next.js stops calling the unhealthy service and fails fast.

HALF-OPEN

A controlled request tests whether the backend has recovered.

CLOSED
   |
   | repeated failures
   v
 OPEN
   |
   | recovery window expires
   v
HALF-OPEN
   |
   +---- success ----> CLOSED
   |
   +---- failure ----> OPEN

Implementing the Breaker in Next.js

The majority of our resilience implementation belongs inside Next.js.

We will create a reusable TypeScript class that can wrap any asynchronous operation: a Magento REST call, GraphQL request, inventory lookup or another external API call.

lib/circuit-breaker.ts
export type CircuitState =
  | 'CLOSED'
  | 'OPEN'
  | 'HALF_OPEN';

export class CircuitOpenError extends Error {
  constructor() {
    super('Circuit is currently open');
    this.name = 'CircuitOpenError';
  }
}

interface CircuitBreakerOptions {
  failureThreshold: number;
  recoveryTimeout: number;
}

export class CircuitBreaker {
  private state: CircuitState = 'CLOSED';
  private failureCount = 0;
  private nextAttempt = 0;
  private halfOpenRequestRunning = false;

  constructor(
    private options: CircuitBreakerOptions
  ) {}

  async execute<T>(
    operation: () => Promise<T>
  ): Promise<T> {

    const now = Date.now();

    if (this.state === 'OPEN') {

      if (now < this.nextAttempt) {
        throw new CircuitOpenError();
      }

      this.state = 'HALF_OPEN';
    }

    if (
      this.state === 'HALF_OPEN' &&
      this.halfOpenRequestRunning
    ) {
      throw new CircuitOpenError();
    }

    if (this.state === 'HALF_OPEN') {
      this.halfOpenRequestRunning = true;
    }

    try {

      const result = await operation();

      this.reset();

      return result;

    } catch (error) {

      this.recordFailure();

      throw error;

    } finally {

      this.halfOpenRequestRunning = false;

    }
  }

  private recordFailure() {

    this.failureCount++;

    if (
      this.state === 'HALF_OPEN' ||
      this.failureCount >=
        this.options.failureThreshold
    ) {

      this.state = 'OPEN';

      this.nextAttempt =
        Date.now() +
        this.options.recoveryTimeout;
    }
  }

  private reset() {
    this.state = 'CLOSED';
    this.failureCount = 0;
    this.nextAttempt = 0;
  }

  getStatus() {
    return {
      state: this.state,
      failureCount: this.failureCount,
      nextAttempt: this.nextAttempt
    };
  }
}

This class does not know anything about Magento. That is deliberate.

The Circuit Breaker is infrastructure-level application logic. The same implementation can protect product APIs, recommendations, search services or any other dependency.

For this example, we will open the circuit after five consecutive failures and wait sixty seconds before testing the upstream service again.

lib/circuits.ts
import { CircuitBreaker } from './circuit-breaker';

export const magentoCircuit =
  new CircuitBreaker({
    failureThreshold: 5,
    recoveryTimeout: 60_000
  });

Where Magento PaaS Fits Into This Design

Magento PaaS remains the authoritative commerce backend. Next.js is not replacing the core commerce platform.

Product information, inventory, prices, carts and order data can continue to originate from Magento APIs.

What changes is the way the storefront consumes those APIs.

Instead of every browser interaction depending directly on Magento availability, Next.js provides a controlled server-side integration layer.

lib/magento.ts
const MAGENTO_URL =
  process.env.MAGENTO_BASE_URL!;

export async function fetchMagentoProducts() {

  const controller =
    new AbortController();

  const timeout =
    setTimeout(() => {
      controller.abort();
    }, 3000);

  try {

    const response = await fetch(
      `${MAGENTO_URL}/rest/V1/products?searchCriteria[pageSize]=12`,
      {
        headers: {
          Accept: 'application/json'
        },
        signal: controller.signal,
        cache: 'no-store'
      }
    );

    if (!response.ok) {

      throw new Error(
        `Magento responded with ${response.status}`
      );
    }

    return await response.json();

  } finally {

    clearTimeout(timeout);

  }
}

The three-second timeout is just as important as the breaker. A circuit breaker without a sensible timeout can still leave requests waiting too long.

In a production commerce project, timeout values should be based on the business operation. A category listing request and an order placement request may require different limits.

Use a Next.js Route Handler as the Storefront Boundary

Next.js Route Handlers are a convenient place to expose a stable storefront endpoint.

The browser requests /api/products. The Route Handler decides whether Magento should be called.

app/api/products/route.ts
import {
  CircuitOpenError
} from '@/lib/circuit-breaker';

import {
  magentoCircuit
} from '@/lib/circuits';

import {
  fetchMagentoProducts
} from '@/lib/magento';

import {
  fallbackProducts
} from '@/lib/fallback-products';

export async function GET() {

  try {

    const data =
      await magentoCircuit.execute(
        () => fetchMagentoProducts()
      );

    return Response.json({
      source: 'magento',
      degraded: false,
      data
    });

  } catch (error) {

    const circuit =
      magentoCircuit.getStatus();

    console.error(
      'Magento product request failed',
      {
        circuit,
        error
      }
    );

    if (
      error instanceof CircuitOpenError
    ) {

      return Response.json({
        source: 'fallback',
        degraded: true,
        reason: 'circuit_open',
        data: fallbackProducts
      });
    }

    return Response.json({
      source: 'fallback',
      degraded: true,
      reason: 'upstream_error',
      data: fallbackProducts
    });
  }
}

From the frontend’s perspective, the integration is now simple. It speaks only to Next.js.

Browser
   |
   v
Next.js /api/products
   |
   v
Circuit Breaker
   |
   +---- OPEN ----------> fallback response
   |
   +---- CLOSED --------> Magento PaaS
                              |
                              +--> success
                              |
                              +--> failure

This is the key architectural benefit: the storefront gains a stable application boundary even when the backend ecosystem becomes temporarily unstable.

The Best Circuit Breaker Includes a Business-Aware Fallback

Simply returning an error is not always the best customer experience.

For catalog content, Next.js may be able to return previously cached products, featured products or a reduced representation.

lib/fallback-products.ts
export const fallbackProducts = [
  {
    id: 'featured-01',
    name: 'Featured Product',
    availability: 'temporarily-unavailable'
  },
  {
    id: 'featured-02',
    name: 'Popular Product',
    availability: 'temporarily-unavailable'
  }
];

The UI can also make degraded mode visible without turning the experience into a hard failure.

app/products/page.tsx
export default async function ProductsPage() {

  const response =
    await fetch(
      `${process.env.APP_URL}/api/products`,
      {
        cache: 'no-store'
      }
    );

  const result =
    await response.json();

  return (
    <main>

      <h1>Products</h1>

      {result.degraded && (
        <p>
          Live inventory is temporarily
          unavailable. Showing available
          catalog information.
        </p>
      )}

      {/* Render product cards */}

    </main>
  );
}
Important

A fallback must match the operation. Showing cached product information can be reasonable. Pretending that a payment or order placement succeeded when Magento never accepted it is not.

Retry and Circuit Breaker Solve Different Problems

Retry logic is useful when an operation has a reasonable chance of succeeding on another attempt.

Circuit Breaker logic is useful when repeated failures indicate that another attempt is probably wasteful.

Pattern Main Question Typical Behaviour
Timeout How long should we wait? Stop one slow request.
Retry Should we try again? Repeat selected transient failures.
Circuit Breaker Should we call at all? Stop traffic to a known unhealthy dependency.
Fallback What should the user receive? Provide controlled degraded behaviour.

Strong resilient systems usually use these patterns together rather than treating one of them as a complete solution.

Production Considerations for Next.js

Our example intentionally stores the circuit state in module memory. That keeps the implementation focused on the pattern itself.

This approach works logically inside a long-running Next.js process, but production platforms may run multiple Node.js processes, containers, regions or serverless instances.

Next.js Instance A
Circuit = OPEN

Next.js Instance B
Circuit = CLOSED

Next.js Instance C
Circuit = CLOSED

Each process may therefore have its own opinion about Magento health.

For many storefront operations, an instance-local breaker is still useful because each individual process protects itself from spending resources on repeated failures.

For a globally coordinated breaker, the state should move into a shared distributed store. That is a separate production architecture decision and not required to understand the core Next.js pattern.

Do Not Count Every Error

A production breaker should also distinguish between an unhealthy backend and a valid application error.

A network timeout, connection reset or HTTP 503 may indicate an availability problem.

A Magento HTTP 400 caused by invalid request data usually means the request is wrong, not that Magento is unavailable.

The second error should generally not move the circuit toward OPEN.

Add Observability

Log state changes instead of treating the breaker as invisible application logic.

example log
{
  "service": "magento-catalog",
  "event": "circuit_opened",
  "failureCount": 5,
  "recoveryTimeoutMs": 60000
}

Useful metrics include upstream response latency, timeout count, circuit state, rejected request count and successful recovery tests.

The Final Architecture

Magento PaaS remains the commerce engine. Next.js remains the customer experience layer.

But Next.js also becomes the resilience boundary protecting that customer experience from failures in downstream commerce systems.

CUSTOMER
   |
   v
NEXT.JS STOREFRONT
   |
   +--> Server Components
   +--> Route Handlers
   +--> Timeout
   +--> Circuit Breaker
   +--> Fallback
   |
   v
MAGENTO / ADOBE COMMERCE PAAS
   |
   +--> Catalog
   +--> Pricing
   +--> Inventory
   +--> Cart
   +--> Orders
   |
   v
ERP / OMS / PAYMENT / TAX / SHIPPING

The biggest lesson is not the TypeScript class itself.

It is the architectural decision to assume that remote services will eventually become slow or unavailable.

Next.js should not continue sending unlimited requests to a dependency that is already known to be unhealthy.

Set sensible timeouts. Track meaningful failures. Open the circuit when necessary. Provide an appropriate fallback. Test recovery carefully and restore traffic only after the upstream service becomes healthy again.

With this approach, a temporary Magento or downstream commerce issue does not automatically have to become a complete storefront incident.

That is where the Circuit Breaker pattern becomes valuable in a modern Next.js + Magento PaaS architecture.