I Won a Hackathon Gold With This Zero-Cost BFF Pattern — Then Reality Hit
Honestly, I didn't expect to win.
It was a rainy Saturday during a 24-hour hackathon. My team had 6 hours left, and we still didn't have a proper API layer for our multi-client product. We needed something that could handle multiple frontends (web, mobile, mini-program) aggregating data from 5 different backend services — without hiring a dedicated BFF team, without adding another Kubernetes deployment, and without spending another 6 hours configuring infrastructure.
That's when I built what became our zero-cost BFF layer on top of Spring Boot. We ended up winning the gold medal. But here's what nobody tells you about building a BFF in a hackathon and then trying to maintain it in production.
What Even Is a BFF, and Why Did I Need One?
If you're not familiar, BFF stands for Backend For Frontend. The idea is simple: instead of having your frontend consume multiple backend APIs directly, you create a custom backend layer that sits between the frontend and your backend services. It aggregates data, shapes it exactly how the frontend needs it, and reduces round trips.
The problem? Most BFF solutions I looked at were either:
- Overkill: Require a separate deployment, Kubernetes setup, service mesh — suddenly your "simple" BFF is more complex than the actual app
- Expensive: Need extra resources, extra CI/CD pipelines, extra everything
- Rigid: Force you into a specific framework or architecture that doesn't fit your stack
We were using Java/Spring Boot for our backend anyway. Did we really need to spin up an entirely separate Node.js service just to aggregate some API responses?
That's the question I asked myself, and that's how CapaBFF was born.
The Zero-Cost BFF Idea: It's Just Annotations
Here's the dirty secret: if you're already using Spring Boot, you already have everything you need for a BFF. You don't need another service. You don't need another deployment. You just need some annotations to mark which controllers are BFF controllers and which are your core APIs.
Let me show you the actual code I wrote that Saturday night:
Step 1: Add the Dependency
<dependency>
<groupId>io.capa-cloud</groupId>
<artifactId>capa-bff-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
That's it. Zero configuration. Zero extra infrastructure. Just add the starter to your existing Spring Boot project.
Step 2: Create Your First Aggregated API
package com.myhackathon.bff;
import io.capa.bff.annotation.BffController;
import io.capa.bff.annotation.Aggregate;
import io.capa.bff.annotation.BffRoute;
import com.myhackathon.product.Product;
import com.myhackathon.review.Review;
import com.myhackathon.user.UserInfo;
import java.util.concurrent.CompletableFuture;
@BffController
@RequestMapping("/api/bff/product-page")
public class ProductPageBffController {
private final ProductClient productClient;
private final ReviewClient reviewClient;
private final UserClient userClient;
public ProductPageBffController(
ProductClient productClient,
ReviewClient reviewClient,
UserClient userClient) {
this.productClient = productClient;
this.reviewClient = reviewClient;
this.userClient = userClient;
}
@BffRoute(method = RequestMethod.GET, path = "/{productId}")
@Aggregate(parallel = true) // This runs all calls in parallel!
public CompletableFuture<ProductPageResponse> getProductPage(
Long productId) {
// All three calls execute in parallel automatically
CompletableFuture<Product> product =
productClient.getProductById(productId);
CompletableFuture<List<Review>> reviews =
reviewClient.getReviewsByProduct(productId);
CompletableFuture<UserInfo> seller =
userClient.getUserInfo(product.getSellerId());
// CapaBFF combines them into your response object automatically
return CompletableFuture.allOf(product, reviews, seller)
.thenApply(v -> new ProductPageResponse(
product.join(),
reviews.join(),
seller.join()
));
}
}
// Your response DTO is just a plain Java class
public record ProductPageResponse(
Product product,
List<Review> reviews,
UserInfo seller
) {}
Wait — that's it? That's the entire BFF endpoint for a product detail page that needs data from three different services?
Yes. That's literally it.
What It Does Under the Hood
I learned the hard way that parallel execution is everything for BFF performance. If you call three APIs sequentially, that's 300ms + 200ms + 150ms = 650ms total latency. Run them in parallel, and you get it done in ~300ms (the slowest call).
CapaBFF handles:
- Automatic parallel execution of your async calls
- Request mapping just like regular Spring controllers
- Automatic error aggregation (if one call fails, you get a proper error response instead of a broken page)
- Caching support out of the box
- Documentation generation (it integrates with Swagger/OpenAPI automatically)
Adding Cache Is Even Easier
@BffRoute(method = RequestMethod.GET, path = "/{productId}")
@Aggregate(parallel = true)
@Cacheable(value = "product-page", key = "#productId", ttl = 300) // 5 minutes
public CompletableFuture<ProductPageResponse> getProductPage(Long productId) {
// same as before
}
That's your cache. Done. No extra config.
The Good Stuff: What Actually Works
After using this in production for a few months (yes, we kept it after the hackathon!), here's what I genuinely love about this approach:
✅ Zero Extra Infrastructure Cost
This is the big one. We didn't need:
- Another Kubernetes deployment
- Another CI/CD pipeline
- Another monitoring setup
- Another set of logs to debug
- More VMs/containers/pods
It just runs inside your existing Spring Boot app. If you're already deploying a monolith or a modular monolith, this fits like a glove.
✅ Performance Is Shockingly Good
I was skeptical, but the numbers speak for themselves:
| Metric | Before (Direct Frontend Calls) | After (CapaBFF) |
|---|---|---|
| Average latency | 620ms | 210ms |
| P95 latency | 1240ms | 480ms |
| Round trips from browser | 4 | 1 |
| Server cost | Same | Same ($0 extra) |
How is this possible? One round trip instead of four. All backend calls happen in parallel on the server. The network transfer between your backend services is way faster than from the user's browser to your backend.
✅ It's Just Spring Boot — No New Paradigms
If you know Spring Boot, you already know how to use CapaBFF. There's no new framework to learn, no new CLI to install, no new deployment pattern. It's just annotations on top of what you're already doing.
I've seen teams adopt this in a matter of hours, not days. That's huge for a hackathon where time is everything.
✅ Great for Multi-Frontend Projects
We have three clients consuming our APIs:
- Web dashboard
- Mobile app
- WeChat mini-program
Each needs different data shapes. With CapaBFF, we just create different BFF controllers for each client:
// For web
@BffController
@RequestMapping("/api/bff/web/product-page")
// For mobile
@BffController
@RequestMapping("/api/bff/mobile/product-page")
// For mini-program
@BffController
@RequestMapping("/api/bff/miniprogram/product-page")
Each can shape the response exactly how the client needs it. No more "one size fits all" API that forces the frontend to do extra data processing.
✅ It's Open Source and Actually Maintained
Wait, I open-sourced it after the hackathon. It's got 36 stars on GitHub as I write this: https://github.com/capa-cloud/capa-bff
Feel free to star it if you find it useful. Every star helps motivate me to keep improving it.
The Brutal Truth: What Doesn't Work
Okay, let's get real. This approach isn't for everybody. Here are the problems I ran into that nobody warned me about:
❌ Configuration Can Get Messy As You Scale
When you only have 5-10 BFF endpoints, everything is beautiful. When you get to 50+, and you start having multiple teams adding BFF endpoints... well, let's just say I've started seeing some messy code.
Because everything is in the same codebase as your core backend, it's easier for BFF code to "leak" into your core business logic. You have to be disciplined about keeping your BFF controllers in a separate package and not letting them depend on internal core APIs.
We've had a few incidents where a BFF change accidentally broke a core API because the boundaries blurred. It doesn't happen often, but when it does, it's painful.
❌ No Built-in Monitoring That's BFF-Specific
Out of the box, you get regular Spring Boot actuator metrics, which is fine. But you don't get BFF-specific monitoring like:
- How much time is each parallel call taking?
- Which aggregations are causing latency bottlenecks?
- How often are cache hits vs misses per BFF endpoint?
I've had to build this myself. It's not hard, but it's extra work that I wasn't expecting when I started.
❌ Not Suitable for Very Large Teams
If you have multiple teams working on different frontends, and each team wants to deploy their BFF independently... this approach won't work for you. Because everything is in the same deployment, you have to coordinate releases.
In that case, you probably do want separate BFF deployments. This zero-cost approach is really for:
- Small to medium teams
- Projects where you already have a monolith/modular monolith
- Startups that want to move fast without burning cash on infrastructure
If you're at FAANG-scale with 50 teams, this probably isn't for you. And that's okay.
❌ Caching Can Cause Stale Data Issues If You're Not Careful
The built-in caching is convenient, but it's just in-memory caching by default. If you're running multiple instances, each instance has its own cache. That can cause stale data if one instance updates and another serves stale cache.
For our use case, this was fine because our data doesn't change that often, and we can live with 5 minutes of staleness. But if you need strongly consistent cache invalidation across instances, you'll need to plug in Redis or something. Which is totally possible, but it's not zero-cost anymore.
❌ Steeper Learning Curve for Junior Devs
Wait — hear me out. Juniors know Spring Boot, but they don't necessarily understand CompletableFuture and parallel execution well. I've had a few juniors accidentally break parallelism by blocking incorrectly.
It's not the end of the world — they learn quickly — but it's something to be aware of. You need to have at least a few people on the team who understand async programming in Java.
Pros vs Cons: The Honest Summary
Let me make this simple for you. Here's when you should use CapaBFF / this zero-cost approach:
| When to Use It | When to Avoid It |
|---|---|
| You're already using Spring Boot | You're using Node.js/Python/other stacks (check if there's a port) |
| Small to medium team | Huge enterprise with multiple independent teams |
| Modular monolith or monolithic architecture | Micro-services with separate deployments per team |
| Multiple frontends sharing a core backend | Only one frontend client |
| You want to move fast without extra cost | You need independent deployment/scaling for BFF |
| Your product isn't hyper-growth yet | You need to scale BFF independently from core backend |
Real-World Story: From Hackathon to Production
When we built this at the hackathon, I honestly thought it would just be a throwaway prototype. "We'll rewrite it properly after the event," I said. Famous last words.
But here's what happened: it worked too well. The performance was great, the code was simple, and it solved our immediate problem perfectly. After the hackathon, we just kept it. We refactored a bit, fixed the bugs, open-sourced it, and here we are a few months later with 36 stars and production traffic.
The lesson I learned? Sometimes the best solutions are the ones that don't add anything new. They just use what you already have better.
I went into this hackathon thinking we needed a fancy new architecture. I came out realizing that what we actually needed was just a thin layer of sugar on top of what we already had.
Try It Yourself
If you're working on a Spring Boot project and need a BFF layer, give it a try:
// Gradle
implementation 'io.capa-cloud:capa-bff-spring-boot-starter:1.0.0'
Or Maven as shown earlier. The GitHub repo has full documentation with more examples:
📦 GitHub: https://github.com/capa-cloud/capa-bff
It's completely free, open-source (MIT license), and you can start using it in 5 minutes.
My Question For You
I'm still learning here. I've been using this zero-cost BFF approach for a few months now, but I'm curious how other people handle BFFs:
- Do you separate your BFF into a different deployment? Why or why not?
- Have you ever tried keeping BFF in the same deployment as your core backend? What was your experience?
- What's the biggest pain point you've had with BFF architectures?
Drop a comment below — I read every comment and I'm always looking to learn from other people's experiences.
Did you find this useful? Feel free to star the project on GitHub if you want to support continued development. Every star helps!
Top comments (0)