Designing an API Layer and BFF Architecture for a Large‑Scale Supply Chain System
This article examines the challenges of a complex supply‑chain system built on Spring Cloud, proposes introducing an API layer to handle aggregation, distributed calls and decoration, and further adopts a Backend‑for‑Frontend (BFF) approach to reduce service coupling, improve client‑specific optimization, and streamline team responsibilities.
In a previously designed supply‑chain system we built, it includes services such as product, sales order, franchisee, store operations, and work orders, with roles like headquarters product management, headquarters store management, franchisee staff, and store personnel, each further subdivided. The system also contains two client apps: one for customers and one for company employees and franchisees.
The overall architecture is shown in the diagram below:
The gateway layer in the diagram is responsible for routing, authentication, monitoring, and rate‑limiting/circuit‑breaking.
Routing: all requests pass through the gateway, which forwards them to the appropriate backend service based on the URI and performs load balancing when multiple instances exist.
Authentication: centralized authentication and authorization for all requests.
Monitoring: records API request data; an API management system provides usage and performance monitoring.
Rate‑limiting / circuit‑breaking: limits traffic spikes and isolates failing services to protect backend resources while preserving user experience.
At first glance the architecture looks perfect and follows the standard Spring Cloud pattern, but it introduces several problems, illustrated by the following examples.
Case 1
Many UI screens need to display data from multiple services—for example, a store‑operator’s home page must show work‑order count, recent work orders, sales‑order data, pending orders, and items below safety stock.
During interface design we struggled to decide which service should host the APIs used by the two client apps, leading to low decision efficiency and inconsistent responsibility boundaries.
We eventually placed the first interface in the store service, resulting in the call relationship shown below:
We placed the second interface in the work‑order service, producing the call relationship shown below:
Case 2
A single user action often needs to modify data in multiple services—for instance, submitting a work order requires updates to inventory, sales‑order status, and the work‑order itself.
Because such cross‑service operations are frequent, services become heavily inter‑dependent, leading to a tangled dependency graph:
This tangled dependency makes technical iteration painful.
To solve the two problems we decided to introduce an abstract API layer.
API Layer
Generally, client‑facing APIs need to satisfy three requirements: aggregation, distributed invocation, and decoration.
Aggregation: an API aggregates data from multiple backend services and returns it to the client.
Distributed invocation: an API may need to call several backend services sequentially to modify data.
Decoration: an API can reshape or filter backend data, e.g., removing fields or wrapping them, to produce the format required by the client.
Therefore we added a dedicated API layer between clients and backend services. The API layer has no database of its own; its main responsibility is to invoke other backend services.
After this redesign the two earlier problems were largely mitigated:
The number of times we debate where to place a particular interface is reduced: aggregation, decoration, and distributed‑call logic stay in the API layer, while data‑access logic remains in the service that owns the data.
Backend service dependencies are dramatically reduced; only the API layer calls the individual services.
However, new issues arise.
Client‑Adaptation Issues
The supply‑chain system serves many clients (App, H5, PC web, mini‑program, etc.), leading to three problems:
Different clients have different UI priorities; for example, the App may require more information on a page than a mini‑program, forcing the same API to implement client‑specific adaptations.
Clients often need minor changes (adding or removing fields), which forces the backend to follow a “data‑minimization” principle and frequently synchronise releases with the client.
These frequent, fine‑grained changes increase the complexity of making the API layer compatible with all clients.
To address this we consider using a Backend‑for‑Frontend (BFF) pattern.
BFF (Backend for Front)
BFF is not a new architecture but a design pattern that creates a dedicated API for each client, allowing client‑specific optimisation and eliminating cross‑client compatibility logic.
In the diagram below each client request passes through the same gateway but is then redirected to its own API service. Because each API service serves only one client type, it can be lightweight and faster.
Additionally, each client can be released independently without being tied to other clients’ schedules.
In practice the system contains nearly 100 services covering retail, supply‑chain, finance, franchisee, after‑sales, customer service, etc., requiring hundreds of developers.
To achieve business decoupling and independent release cycles, each department maintains its own API service, and the App and PC front‑ends adopt componentisation accordingly. The resulting architecture is shown below:
Technical implementation remains based on Spring Cloud:
The roles of the components are:
Gateway: implemented with Spring Cloud Zuul, registers API services in ZooKeeper and forwards requests via Feign.
API Service: a Spring Web service without its own database, responsible for aggregation, distributed calls, and data decoration, invoking backend services via Feign.
Backend Service: a Spring Web service that owns its database and cache.
Although this design looks perfect, it introduces code duplication across APIs.
Teams handle duplication in three ways:
Package shared code into a JAR that multiple API services depend on.
Extract shared logic into a separate “CommonAPI” service that other APIs call.
Leave the duplicated code in place when the duplication is minimal and the maintenance cost is lower than the overhead of a shared library or service.
If an API merely proxies calls without any transformation, it can be considered a simple pass‑through layer.
Some proposals to eliminate such “pure proxy” APIs include:
Let the gateway bypass the API layer and call backend services directly—rejected because it breaks layering.
Introduce an interceptor in the API layer that, when a URI has no matching controller, forwards the request directly to the appropriate backend service—rejected due to added complexity and limited benefit.
After weighing options, the team decided to keep the straightforward proxy code.
Division of Labor Between Backend and API Teams
Dedicated API teams own the API services, while backend services are grouped by domain into smaller teams.
This division gives the API team a holistic view of all services and prevents overlapping work, but it also means the API team’s business logic is relatively simple, which can affect long‑term retention.
Overall, the article demonstrates how introducing an API layer and BFF pattern can address service coupling, client‑specific requirements, and team organization in a large‑scale Spring Cloud‑based supply‑chain system.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.