In a complex microservices landscape, efficiently orchestrating multiple service calls while serving diverse frontend requirements has become increasingly challenging. This complexity can lead to increased load time, heightened network latency, over-fetching of data, and inadequate error handling. Organizations often struggle to manage these issues, which can impact reliability and performance across different client applications.
The Backend for Frontend (BFF) pattern provides a solution by tailoring backend services to the specific needs of various frontend applications. When implemented with Orkes Conductor, this approach enhances application performance by ensuring efficient orchestration of services, optimizing responses for each client, and improving overall reliability.
As we proceed through this article, we'll explore detailed implementations and best practices for using an orchestration platform like Orkes Conductor as a BFF layer. The examples will demonstrate how this approach can solve real-world challenges in modern application architecture.
Backend for Frontend (BFF) is an architectural pattern for creating a separate backend service for each frontend client (e.g., web browser, mobile app, desktop app) instead of having a one-size-fits-all API. First pioneered by SoundCloud in 2013, BFF allows organizations to:
Traditional BFF implementations typically involve building separate services from scratch for each client type, leading to code duplication and increased maintenance overhead. An approach using orchestration enables teams to swiftly build, iterate, and maintain multiple BFF layers that address development challenges illustrated below.
When building frontend applications that interact with different services, development teams often face several API-related challenges:
The BFF pattern provides a dedicated backend layer for each frontend, offering an elegant solution to these challenges. Using BFF, teams can handle these complexities server-side rather than burdening the frontend applications.
Conductor, an open-source workflow orchestration engine initially developed by Netflix and now maintained by Orkes, brings an effective approach to implementing the BFF pattern. Unlike traditional BFF implementations that might require building separate services from scratch, Conductor provides a robust foundation for orchestrating microservices calls, handling complex workflows, and managing the specific requirements of different front-end clients.
When implementing BFF with Conductor, each frontend client type (web, mobile, desktop) can have its own dedicated workflow definition that orchestrates the underlying microservices in the most optimal way:
[Mobile App] → [Mobile-Optimized Workflow] → [Conductor Engine] → [Microservices]
[Web App] → [Web-Optimized Workflow] → [Conductor Engine] → [Microservices]
[Desktop] → [Desktop-Optimized Workflow] → [Conductor Orchestration] → [Microservices]
The capabilities of an orchestration tool like Orkes Conductor naturally support the implementation needs of a BFF layer. Beyond the core benefits of using workflow orchestration for coordinating service calls, a Conductor-based BFF layer also unlocks these features out of the box:
Furthermore, the true power of using Conductor as a BFF layer lies in its ability to combine workflow orchestration with client-specific optimizations:
Consider an e-commerce platform where the mobile app, web interface, and desktop application all need to display product details. Below is an example workflow where a web client is displaying the product details of the e-commerce platform:
// Example Conductor workflow definition for web client with data aggregation
{
"name": "web_product_details_workflow",
"description": "Workflow for web product details",
"version": 1,
"tasks": [
{
"name": "fetch_basic_product_info",
"taskReferenceName": "product_info",
"type": "HTTP",
"inputParameters": {
"http_request": {
"uri": "${workflow.input.productServiceUrl}",
"method": "GET"
}
}
},
{
"name": "fetch_product_reviews",
"taskReferenceName": "reviews",
"type": "HTTP",
"inputParameters": {
"http_request": {
"uri": "${workflow.input.reviewServiceUrl}",
"method": "GET",
"query": {
"productId": "${product_info.output.productId}"
}
}
}
},
{
"name": "fetch_recommendations",
"taskReferenceName": "recommendations",
"type": "HTTP",
"inputParameters": {
"http_request": {
"uri": "${workflow.input.recommendationServiceUrl}",
"method": "GET",
"query": {
"userId": "${workflow.input.userId}"
}
}
}
},
{
"name": "optimize_images",
"taskReferenceName": "web_images",
"type": "SIMPLE",
"inputParameters": {
"images": "${product_info.output.images}",
"targetResolution": "web"
}
},
{
"name": "data_aggregation",
"taskReferenceName": "aggregate_data",
"type": "SIMPLE",
"inputParameters": {
"productInfo": "${product_info.output}",
"reviews": "${reviews.output}",
"recommendations": "${recommendations.output}",
"images": "${web_images.output}"
}
}
],
"outputParameters": {
"finalOutput": "${aggregate_data.output}"
}
}
With Conductor as the BFF layer, the mobile client flow for displaying product details can be tuned to mobile-specific requirements, such as optimizing image sizes, as demonstrated in the following workflow:
// Example Conductor workflow definition for mobile client
{
"name": "mobile_product_details_workflow",
"description": "Workflow for mobile product details",
"version": 1,
"tasks": [
{
"name": "fetch_basic_product_info",
"taskReferenceName": "product_info",
"type": "HTTP",
"inputParameters": {
"http_request": {
"uri": "${workflow.input.productServiceUrl}",
"method": "GET"
}
}
},
{
"name": "optimize_images",
"taskReferenceName": "mobile_images",
"type": "SIMPLE",
"inputParameters": {
"images": "${product_info.output.images}",
"targetResolution": "mobile"
}
}
]
}
Using Conductor means having a robust, maintainable, and scalable architecture that can evolve with your application's needs while providing optimal experiences for each client platform.
Here is a comparison of the implementation effort required in a traditional BFF set-up versus a Conductor-based set-up.
Area | Traditional BFF | Conductor-based BFF |
---|---|---|
Setup Complexity | High - requires separate services | Low - configurable workflows[1] |
Code Reusability | Limited | High - shared task and workflow definitions |
Monitoring | Custom implementation needed | Built-in monitoring and visualization |
Error Handling | Custom implementation needed | Built-in retry and recovery mechanisms |
Scalability | Requires additional infrastructure | Built-in infrastructure for scalability |
Maintenance | High - multiple codebases | Low - centralized platform for workflow definitions |
Let's implement a Backend-For-Frontend (BFF) layer using Orkes Conductor. We'll create a complete flow that fetches and transforms product data from a backend service. In this example, we are using a single endpoint, but Conductor also easily handles cases for fetching and combining data from multiple services.
There are two options for accessing Conductor:
Production Environment
Development Environment (Recommended for this tutorial)
To configure programmatic access:
keyId
keySecret
(shown only once)Let's implement the connection between Orkes Conductor and your frontend application using the JavaScript SDK.
Installing dependencies
First, install the Orkes Conductor SDK:
# Using yarn
yarn add @io-orkes/conductor-javascript
# Using npm
npm install --save @io-orkes/conductor-javascript
Configuration setup
Create a configuration file to manage your Conductor connection details:
// src/config/conductor.js
export const config = {
keyId: import.meta.env.VITE_KEY,
keySecret: import.meta.env.VITE_KEY_SECRET,
serverUrl: import.meta.env.VITE_SERVER_URL, // https://play.orkes.io/api for Playground
};
Creating the Conductor client hook
Implement a custom hook to manage the Conductor client initialization:
// src/hooks/useConductor.js
import {
ConductorClient,
orkesConductorClient,
} from "@io-orkes/conductor-javascript";
import { useEffect, useState } from "react";
import { config } from "../config/conductor";
async function fetchClient() {
try {
const clientPromise = orkesConductorClient(config);
const client = await clientPromise;
return client;
} catch (error) {
console.error("Error initializing client:", error);
throw error;
}
}
export const useConductor = () => {
const [conductorClient, setConductorClient] = useState();
const [error, setError] = useState(null);
useEffect(() => {
const initializeClient = async () => {
try {
const client = await fetchClient();
setConductorClient(client);
} catch (err) {
setError(err);
}
};
initializeClient();
}, []);
return {
conductorClient,
error,
};
};
After setting up the Conductor client, you can leverage its capabilities across your application components. While the client provides access to all Conductor resources, our focus for the BFF layer will be the workflowResource
.
Before utilizing the conductorClient
, let's create a workflow in Orkes Playground called workflow fetch_and_transform_data
, which is designed to retrieve product data from an endpoint and apply transformations. This example can be adapted to suit various use cases.
Sample data source
For demonstration purposes, we'll use a mock API endpoint:
https://fake-store-api.mock.beeceptor.com/api/products
This endpoint returns product data in the following format:
[
{
"product_id": 1,
"name": "Smartphone",
"description": "High-end smartphone with advanced features.",
"price": 599.99,
"unit": "Piece",
"image": "https://example.com/images/smartphone.jpg",
"discount": 10,
"availability": true,
"brand": "BrandX",
"category": "Electronics",
"rating": 4.5,
"reviews": [
{
"user_id": 1,
"rating": 5,
"comment": "Great phone with a superb camera!"
},
{
"user_id": 2,
"rating": 4,
"comment": "Good performance, but the battery life could be better."
}
]
}
// Additional products...
]
Workflow implementation
Our workflow will fetch this data and apply transformations using two main tasks:
This structure allows the BFF layer to:
Data transformations
The Inline task for data transformation will apply the following modifications:
Here's the workflow definition:
{
"name": "fetch_and_transform_data",
"description": "Fetch product data and apply transformations",
"version": 1,
"tasks": [
{
"name": "http",
"taskReferenceName": "http_ref",
"inputParameters": {
"uri": "https://fake-store-api.mock.beeceptor.com/api/products",
"method": "GET",
"accept": "application/json",
"contentType": "application/json",
"encode": true
},
"type": "HTTP"
},
{
"name": "inline",
"taskReferenceName": "inline_olx_ref",
"inputParameters": {
"expression": "// Transformation logic",
"evaluatorType": "graaljs",
"data": "${http_ref.output.response.body}"
},
"type": "INLINE"
}
]
// Additional workflow parameters...
}
Find the full workflow definition here: fetch_and_transform_data
This workflow showcases how Orkes Conductor can be used as a powerful Backend for Frontend (BFF) layer. You can customize the workflow based on your specific requirements, utilizing various task types available in Conductor to suit your use case. Let's break down its current components and functionality:
{
"name": "http",
"taskReferenceName": "http_ref",
"inputParameters": {
"uri": "https://fake-store-api.mock.beeceptor.com/api/products",
"method": "GET",
"accept": "application/json",
"contentType": "application/json",
"encode": true
},
"type": "HTTP"
}
This task:
{
"name": "inline",
"taskReferenceName": "inline_olx_ref",
"inputParameters": {
"expression": "...", // JavaScript transformation function
"evaluatorType": "graaljs",
"data": "${http_ref.output.response.body}"
},
"type": "INLINE"
}
This task:
Transformed data structure
After processing, the data will be restructured as follows:
[
{
"specs": {
"unit": "Piece",
"category": "Electronics",
"brand": "BrandX"
},
"image": {
"src": "https://example.com/images/smartphone_large.jpg",
"alt": "Smartphone"
},
"reviews": [
{
"rating": 5,
"comment": "Great phone with a superb camera!",
"id": 1
},
{
"rating": 4,
"comment": "Good performance, but the battery life could be better.",
"id": 2
}
],
"seoMeta": {
"description": "Buy Smartphone - High-end smartphone with advanced features....",
"title": "Smartphone | BrandX"
},
"price": {
"discounted": "$539.99",
"discountPercentage": 10,
"original": "$599.99"
},
"name": "Smartphone",
"rating": {
"average": "4.5",
"count": 2
},
"description": "High-end smartphone with advanced features.",
"id": 1,
"stock": {
"isAvailable": true,
"status": "In Stock"
}
}
// Additional transformed products...
]
With the workflow created, the final step is to connect your Conductor workflow to your frontend. This is as simple as starting the workflow to retrieve the product data.
Example Usage in a React Component
Now let's go back to your code and see how you can execute this workflow in your project.
import { useCallback, useEffect, useState } from "react";
import "./App.css";
import { useConductor } from "./hooks/useConductor";
function ProductList() {
const { conductorClient } = useConductor();
const [transformedData, setTransformedData] = useState(null);
const getTransformedProductData = useCallback(async () => {
if (conductorClient) {
const executionId = await conductorClient.workflowResource.startWorkflow({
name: "fetch_and_transform_data",
version: 1,
});
const executionData =
await conductorClient.workflowResource.getExecutionStatus(executionId);
if (executionData.status === "COMPLETED") {
setTransformedData(executionData.output?.result);
}
}
}, [conductorClient]);
useEffect(() => {
getTransformedProductData();
}, [getTransformedProductData]);
return (
<div className="container">
{transformedData && transformedData.length > 0 ? (
transformedData.map((product) => (
<div key={product.id} className="product-card">
<img
src={product.image.src}
alt={product.image.alt}
className="product-image"
/>
<div className="product-details">
<h2 className="product-name">{product.name}</h2>
<p className="product-description">{product.description}</p>
<p className="product-info">
<span className="label">Category:</span>{" "}
{product.specs.category}
</p>
<p className="product-info">
<span className="label">Brand:</span> {product.specs.brand}
</p>
<p className="product-price">
<span className="discounted">{product.price.discounted}</span>{" "}
<span className="original">{product.price.original}</span>{" "}
<span className="discount">
({product.price.discountPercentage}% off)
</span>
</p>
<p className="product-stock">
<span
className={`status ${
product.stock.isAvailable ? "in-stock" : "out-of-stock"
}`}
>
{product.stock.isAvailable
? product.stock.status
: "Out of Stock"}
</span>
</p>
<p className="product-rating">
<span className="label">Rating:</span> {product.rating.average}{" "}
({product.rating.count} reviews)
</p>
<div className="reviews">
<h3>Reviews:</h3>
{product?.reviews?.map((review) => (
<div key={review.id} className="review">
<p>Rating: {review.rating}</p>
<p>{review.comment}</p>
</div>
))}
</div>
</div>
</div>
))
) : (
<p className="no-products">No products available.</p>
)}
</div>
);
}
export default ProductList;
In the example above, once you start the workflow, it returns an executionId
. Using the executionId
, the project retrieves the executionData
by calling the getExecutionStatus
method available in workflowResource
. If executionData.status
is "COMPLETED", Conductor returns executionData.output.result
, which will contain your transformed data that can be used for your frontend display.
Implementing the Backend for Frontend pattern using Orkes Conductor offers a powerful solution for managing complex microservices architectures and delivering optimized data to diverse frontend clients.
By leveraging Conductor's workflow orchestration capabilities, organizations can build highly effective BFF layers that enhance both developer productivity and application performance.
—
Orkes Cloud is a fully managed and hosted Conductor service that can scale seamlessly to meet your needs. When you use Conductor via Orkes Cloud, your engineers don’t need to worry about setting up, tuning, patching, and managing high-performance Conductor clusters. Try it out with our 14-day free trial for Orkes Cloud.