DEV Community

KevinTen
KevinTen

Posted on

I Won a Hackathon Gold With This Zero-Cost BFF Pattern — Then Reality Hit

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:

  1. Overkill: Require a separate deployment, Kubernetes setup, service mesh — suddenly your "simple" BFF is more complex than the actual app
  2. Expensive: Need extra resources, extra CI/CD pipelines, extra everything
  3. 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>
Enter fullscreen mode Exit fullscreen mode

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
) {}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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)