The "Backend For Frontend" (BFF) Pattern
Why It's the Quiet Champion Your Domain-Driven App Needs
Table of contents
- Why BFF and DDD Form a Winning Partnership
- Wrapping Up: Why You Need a BFF
It’s exhausting, isn’t it? Crafting this pristine backend, where services are finely tuned with domain logic, validation, and business rules, all while your poor frontend is left out in the cold, making endless HTTP requests to multiple services, piecing it all together—like some ill-fated scavenger hunt.
Enter the Backend for Frontend (BFF) pattern—the unsung hero in this story. In today’s post, we’ll look at why BFFs work so well in tandem with Domain-Driven Design (DDD), and why they should be an integral part of your application architecture. For a little bonus, we’ll sprinkle in some practical examples with AWS CDK (including a neat workaround for attaching usage plans and API keys via CDK Aspects).
Why BFF and DDD Form a Winning Partnership
As your system grows—more domains, more APIs, more everything—the complexities on the frontend will increase accordingly. Domain services might be neatly separated at the backend, but the burden of aggregating and juggling their data will inevitably fall on the frontend developers. It's a bit unfair—and terribly inefficient. That's where BFFs come in, acting as a dedicated middle layer, transforming backend complexity into frontend simplicity.
Let's break it down.
1. Streamlining Frontend Requests
The Problem Without BFF: When your frontend directly interacts with various domain services, you quickly end up with a mess of HTTP calls, each dealing with its own service. Imagine the complexity of interacting with a dozen APIs from a simple React app—the requests, the retry logic, the sequence of events.
With BFF: The frontend doesn’t need to know or care about the multitude of services lurking in the depths of your backend kingdom. The BFF abstracts all that, providing a single API that interacts with the necessary services and returns a nicely structured response.
Here’s a simple example of how BFF comes to the rescue using AWS API Gateway and AWS Lambda. Your frontend can gracefully call a single endpoint to retrieve detailed information about, say, a user or a product. The BFF takes on the dirty work—fetching data from multiple services like Users
, Inventory
, and Recommendations
.
2. Keeping Domain Responsibilities Where They Belong
The Problem Without BFF: Let’s say you’re building the frontend for an e-commerce platform. You need product details, which means calling the Pricing
, Inventory
, and Reviews
domain services, among others.
What happens? Your frontend is now responsible for making all these calls, sequencing them appropriately, aggregating the results, and dealing with all sorts of edge cases—timeouts, failed calls, out-of-sync responses. Complexity creeps in fast, and your frontend is now shouldering work it was never meant to handle.
With BFF: The BFF can take responsibility for orchestrating these calls. It doesn't merely proxy the requests—it performs the necessary API calls to gather the data, handles errors when they arise, and packages everything as an elegant response for the frontend.
A classic BFF endpoint would look something like /product/{productId}/details
. The API call behind the scenes automatically handles pricing, availability, reviews, and product images. The frontend, blissfully unaware of the chaos happening below the surface, receives a neatly compiled JSON response.
3. Performance and Security Benefits
Without BFF: More direct API calls from the frontend mean more round-trips, more latency, and the potential for increased attack surfaces. You’re also pushing API key management onto the frontend, and that’s just bad practice.
With BFF: The BFF optimizes how backend calls are handled. It can batch and parallelize requests, reducing latency by minimizing unnecessary round-trips. By serving as a single point of contact for all frontend calls, the BFF reduces the risk of exposing sensitive backend services directly to the public.
And yes, in AWS, you can manage API Gateway’s keys and usage plans from the BFF side, ensuring that only appropriate calls are routed, secured, and handled efficiently.
Putting It All Together: A Practical Example with AWS CDK
Building a BFF can seem daunting, but thankfully the AWS Cloud Development Kit (CDK) comes to the rescue. Let’s walk through how you can deploy a BFF using API Gateway, Lambda, and some CloudFormation magic.
Setting Up The Basics
You’ll need the usual suspects:
API Gateway to expose your BFF.
AWS Lambda to process the backend logic and communicate with other domain services.
API Keys and Usage Plans, because we do care about rate-limiting and access control.
Here’s where things get a bit tricky—out of the box, CDK’s API Gateway constructs don’t play nicely when trying to link specific APIs to usage plans when the APIs are coming from other projects. Thankfully, you can use CDK Aspects to work around this.
class AddApisToUsagePlanAspect implements IAspect {
constructor(private readonly externalApiIds: string[]) {}
public visit(node: IConstruct): void {
if (node instanceof UsagePlan) {
const usagePlan = node as UsagePlan;
const cfnUsagePlan = usagePlan.node.defaultChild as CfnUsagePlan;
cfnUsagePlan.apiStages = this.externalApiIds.map(apiId => ({
apiId, stage: "prod",
})) as any;
}
}
}
Let’s break this down so it doesn't feel like we’re just throwing jargon your way and calling it a day:
Aspect Class: We created an Aspect, which is just a bit of code that inspects each construct in your CDK stack. In our case, we looked for the UsagePlan constructs.
CloudFormation Hackery: We dive into the underlying CloudFormation representation of the usage plan (i.e.,
CfnUsagePlan
), pull out the existingapiStages
(if any), and append our shiny new list of external API IDs to this array. This way, AWS knows that these external APIs should be attached and governed by the same usage plan.Attach External APIs: CDK didn’t natively allow us to link multiple external APIs under a single usage plan the way we needed. Thanks to CDK Aspects, we were able to surgically inject the correct API IDs into the API Gateway definition, ensuring all external API calls route through the correct usage plan and API key management without manually editing the generated CloudFormation.
Want to Go Deeper into CDK Aspects?
If this introduced you to the wondrous world of CDK Aspects but left you wanting more, you're in luck! I go waaaaay deeper into the theory and practical use cases of Aspects in my other blog post, Breaking Bad Practices with CDK Aspects Whether you're struggling with modifying CDK constructs mid-flight, dealing with compliance checks, or just looking for creative ways to improve your CDK game, that post has got you covered. Go peek at it! I promise you’ll walk away inspired.
With the CDK Aspect out of the way…
Here’s a simplified example of what your BFF Stack might look like:
export class BlogBffStack extends Stack {
constructor(scope: Construct, id: string, props: BlogBffStackProps) {
super(scope, id, props);
const apiKey = new ApiKey(this, 'ApiKey', { apiKeyName: 'BFFApiKey' });
const api = new RestApi(this, 'BFFApi', {
description: 'Backend for Frontend API',
endpointConfiguration: { types: [EndpointType.REGIONAL] },
});
const usagePlan = new UsagePlan(this, 'UsagePlan');
usagePlan.addApiKey(apiKey);
Aspects.of(this).add(new AddApisToUsagePlanAspect([api.restApiId, ...Object.values(props.externalApis)]));
Object.entries(props.externalApis).forEach(([apiName, apiId]) => {
const integration = new HttpIntegration(`https://${apiId}.execute-api.us-east-1.amazonaws.com/prod/{proxy}`);
api.root.addResource(apiName.toLowerCase()).addProxy({ anyMethod: true, defaultIntegration: integration });
});
}
}
This will give you a properly configured BFF API, safely hidden behind an API Gateway, and all access controlled via API keys. The frontend, in turn, gets a consistent and reliable interface without ever knowing what’s happening behind the scenes.
Advanced Orchestration in the BFF
Note: My GitHub repo doesn't actually cover this portion directly, but this is how a BFF endpoint could work in practice to merge multiple sources of information on the backend.
Your BFF doesn’t need to merely pass requests through—it can take on roles like service aggregation and orchestration. Here’s a sample Lambda function that aggregates product data from various domain services:
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
const productId = event.pathParameters?.productId;
const externalApis = JSON.parse(process.env.EXTERNAL_APIS!);
try {
const [priceResp, inventoryResp, reviewsResp] = await Promise.all([
fetch(`https://${externalApis["Pricing"]}.execute-api.us-east-1.amazonaws.com/prod/price/${productId}`),
fetch(`https://${externalApis["Inventory"]}.execute-api.us-east-1.amazonaws.com/prod/inventory/${productId}`),
fetch(`https://${externalApis["Reviews"]}.execute-api.us-east-1.amazonaws.com/prod/reviews/${productId}`)
]);
const [priceData, inventoryData, reviewsData] = await Promise.all([
priceResp.json(),
inventoryResp.json(),
reviewsResp.json()
]);
const responseData = {
productId,
pricing: priceData,
inventory: inventoryData,
reviews: reviewsData,
};
return {
statusCode: 200,
body: JSON.stringify(responseData),
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ message: 'Failed to retrieve product details.' }),
};
}
};
This Lambda function executes parallel requests to Pricing
, Inventory
, and Review
services, compiles the data, and returns it as a single response, reducing latency and simplifying frontend calls.
Full Code Available on GitHub 💾
All code featured in this article—including the BFF stack, the proxy configuration, the AddApisToUsagePlanAspect, and Lambda functions—is available at this GitHub repository:
So feel free to clone it, test it, and even poke around to see how it all works together in glorious AWS harmony. 🎶
Wrapping Up: Why You Need a BFF
Using a BFF in conjunction with Domain-Driven Design isn't just a nice-to-have; it’s essential. You not only reduce the burden on your frontend but also streamline backend complexity into manageable, secure, and high-performing APIs.
Here’s what you gain:
Reduced complexity for your frontend with a single call to a BFF that handles all orchestration.
Optimized performance via parallel tasks, batching, and reduced round-trips.
Enhanced security by hiding your backend services behind one simplified API surface controlled through API keys.
And with powerful tools like AWS CDK, building and deploying a BFF is no longer a headache—you can set up everything automatically while ensuring you have tight monitoring, access control, and scalability.
In short, your frontend will breathe a sigh of relief, and your backend will perform smoothly without bogging down the user experience. Really, what's not to love?
May your backend architecture remain clean, and your frontend blissfully unaware.
Happy coding—and may your BFF always have your frontend’s back! 👊