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.
Understanding the BFF pattern
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:
- Tailor backend behaviors to specific frontend needs
- Optimize data transfer for different client capabilities
- Manage client-specific authentication and security requirements
- Reduce unnecessary data transmission
- Handle varying connection speeds and reliability requirements
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.
Tackling API development challenges using BFF
When building frontend applications that interact with different services, development teams often face several API-related challenges:
- Frontend applications need to coordinate multiple microservice calls in a specific sequence, which can become complex to manage directly from the client.
- Different frontend clients (web, mobile, IoT) require different error handling strategies and retry mechanisms.
- Each frontend type may need unique business workflow variations.
- Various client types have different data requirements and performance constraints for optimized responses.
- Teams need clear visibility into how their frontend applications interact with backend services.
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.
Why use Conductor for BFF?
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 Conductor advantage in BFF implementation
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:
- Workflow as Code: Define complex service orchestrations using JSON or code
- Visual Workflow Management: Build, debug, and monitor workflows using an intuitive UI
- Built-in Error Handling: Automatic retry mechanisms and failure recovery
- Scalability: Handle millions of concurrent workflows
- Platform Independence: Support for multiple programming languages and frameworks
Furthermore, the true power of using Conductor as a BFF layer lies in its ability to combine workflow orchestration with client-specific optimizations:
- Reusable Workflow and Task Definitions: Share common resources across different client workflows
- Dynamic Task Routing: Route requests to different service instances based on client requirements
- Version Control: Manage different workflow versions as client requirements evolve
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.
Conductor vs traditional BFF implementation
Here is a comparison of the implementation effort required in a traditional BFF set-up versus a Conductor-based set-up.
Implementing Backend for Frontend with Orkes Conductor
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.
Step 1: Set up Conductor access
There are two options for accessing Conductor:
- Production Environment
- Sign up at cloud.orkes.io
- Suitable for production deployments
- Development Environment (Recommended for this tutorial)
- Use Orkes Playground play.orkes.io
- Perfect for learning and prototyping
To configure programmatic access:
- Create an account at play.orkes.io.
- Go to Access Control > Applications.
- Create a new application.
- Generate access credentials:
- Note down your
keyId
- Securely store your
keySecret
(shown only once) - Configure required permissions for:
- Worker API access
- MetaData API access
- Specific workflow/task permissions
Step 2: Connect Conductor with your project
Let's implement the connection between Orkes Conductor and your frontend application using the JavaScript SDK.
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
.
Step 3: Create a workflow
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:
- An HTTP task to retrieve the data
- An Inline task to transform the data
This structure allows the BFF layer to:
- Fetch data from backend services
- Transform and optimize the data for specific frontend needs
- Provide a single, optimized endpoint via the Conductor SDK for the frontend to consume
Data transformations
The Inline task for data transformation will apply the following modifications:
- Calculate average ratings
- Format prices
- Calculate discounted prices
- Optimize image URLs
- Add SEO metadata
- Restructure the data for easier consumption by the frontend
Workflow Definition
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:
Task 1: HTTP Task
{
"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:
- Fetches product data from a specified API endpoint
- Handles the HTTP request details (method, headers, etc.)
- Provides a clean interface for making HTTP requests within the workflow
Task 2: Inline Task
{
"name": "inline",
"taskReferenceName": "inline_olx_ref",
"inputParameters": {
"expression": "...", // JavaScript transformation function
"evaluatorType": "graaljs",
"data": "${http_ref.output.response.body}"
},
"type": "INLINE"
}
This task:
- Takes the output from the HTTP task as input
- Applies a complex transformation to the data using JavaScript
- Demonstrates how business logic can be embedded directly in the workflow
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...
]
Step 4: Connect the Conductor BFF layer to your frontend
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.
Best Practices for BFF with Conductor
- Design Client-Specific Workflows: Create separate workflows for different client types (web, mobile, desktop) to optimize data delivery.
- Leverage Conductor's Task Library: Utilize built-in tasks for common operations to reduce custom code.
- Implement Proper Error Handling: Use Conductor's retry mechanisms and failure workflows for robust error management.
- Monitor and Optimize: Regularly review workflow execution metrics to identify and resolve performance bottlenecks.
- Version Control Workflows: Maintain different versions of workflows to support gradual rollouts and backward compatibility.
- Use Parameterized Workflows: Create flexible workflows that can adapt to different input parameters for reusability.
- Implement Caching Strategies: Use Conductor's caching capabilities to improve response times for frequently requested data.
Summing up: Using Conductor as a BFF layer
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.
- Data Aggregation: Easily fetch and combine data from multiple backend services.
- Custom Transformations: Apply complex, tailored transformations to suit specific frontend needs, such as adding crucial SEO metadata for web applications.
- Performance Optimization: Perform server-side calculations and formatting, reducing frontend workload.
- Flexibility: Quickly modify workflows to accommodate changing frontend requirements without altering backend services.
- Centralized Business Logic: Ensure consistency across different frontends by centralizing complex operations.
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.