<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: KevinTen</title>
    <description>The latest articles on DEV Community by KevinTen (@kevinten10).</description>
    <link>https://dev.to/kevinten10</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3834064%2Fe66ac660-2133-4f24-92cf-35674e6d1f61.jpeg</url>
      <title>DEV Community: KevinTen</title>
      <link>https://dev.to/kevinten10</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kevinten10"/>
    <language>en</language>
    <item>
      <title>Building Capa-BFF: The Three Design Principles Behind This Zero-Cost BFF Solution</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Sun, 28 Jun 2026 16:09:58 +0000</pubDate>
      <link>https://dev.to/kevinten10/building-capa-bff-the-three-design-principles-behind-this-zero-cost-bff-solution-e0n</link>
      <guid>https://dev.to/kevinten10/building-capa-bff-the-three-design-principles-behind-this-zero-cost-bff-solution-e0n</guid>
      <description>&lt;h1&gt;
  
  
  Building Capa-BFF: The Three Design Principles Behind This Zero-Cost BFF Solution
&lt;/h1&gt;

&lt;p&gt;Honestly, I didn't expect to be writing this post. When I first started working with Capa-BFF three months ago, I thought it was just another hackathon project that would die after the competition ended. But here we are — people are actually using it, asking questions about how it works, and genuinely interested in the design decisions behind it.&lt;/p&gt;

&lt;p&gt;So here's the thing: after using it in production for a few months and reading through the source code multiple times, I've come to appreciate the simplicity of its design. It's not the most complex BFF solution out there, but that's exactly the point. Sometimes the best solutions are the ones that get out of your way and let you get stuff done.&lt;/p&gt;

&lt;p&gt;Let me walk you through the three core design principles that make Capa-BFF work, and why I think they're actually pretty brilliant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Problem Are We Even Solving Here?
&lt;/h2&gt;

&lt;p&gt;Before we jump into the code, let me remind you why BFFs exist in the first place. If you're working on a project with multiple clients (web, mobile, different devices), you've probably run into this issue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The backend writes APIs for one client, and then another client needs different data, so you have to change the API again&lt;/li&gt;
&lt;li&gt;Frontend developers waste time waiting for backend changes just to get the data shaped differently&lt;/li&gt;
&lt;li&gt;Solutions like GraphQL are powerful but require a whole separate layer of development that you might not need&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I learned this the hard way on a previous project. We spent two weeks setting up GraphQL, writing schemas, integrating everything — and then the project scope changed, and half of that work was useless. That's when I started thinking: do we really need all that complexity just to let frontend get the data it needs?&lt;/p&gt;

&lt;p&gt;Enter Capa-BFF. The promise is "zero-cost BFF" — you drop it into your existing SpringBoot app, write a JSON config, and you're done. No extra deployment, no new services to maintain. Sounds too good to be true, right? Let's look under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 1: Dynamic Invocation with the Triple Pattern
&lt;/h2&gt;

&lt;p&gt;The first core principle is &lt;strong&gt;dynamic invocation&lt;/strong&gt; using the (appId, methodName, data) triple.&lt;/p&gt;

&lt;p&gt;Here's the thing: any service call, no matter what framework you're using (Dubbo, SpringCloud, Istio, whatever), eventually boils down to three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which application do you want to call? (appId)&lt;/li&gt;
&lt;li&gt;Which method on that application? (methodName)&lt;/li&gt;
&lt;li&gt;What data do you send it? (data)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. Capa-BFF takes this observation and runs with it. Instead of trying to create some fancy new abstraction, they just embrace this reality. Any service call can be represented as a triple.&lt;/p&gt;

&lt;p&gt;Let me show you what that looks like in practice with a real example. Suppose you're building a content platform where you need to get KOL details and then get their application information. Here's how you'd write that in Capa-BFF's DSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"20725.gscontentcenterservice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"getkoldetail"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"kolNo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"kolOrderDetail.id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kol.id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"kolOrderDetail.applyId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kol.applyId"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"getkolapplydetail"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${kol.applyId}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"user.userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user.id"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait, that's just JSON. There's no special schema language to learn, no complicated CLI to generate types. It's just JSON. That's the beauty of it.&lt;/p&gt;

&lt;p&gt;The Capa-BFF parser takes this JSON and converts each method call into a triple. That's it. No magic, no complicated reflection tricks (well, okay, some reflection, but it's hidden from you).&lt;/p&gt;

&lt;p&gt;I remember when I first saw this, I thought "that's too simple — how can this work?" But then I realized: simplicity is the point. We've been conditioned to think that solving hard problems requires complex solutions, but sometimes the opposite is true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 2: Alias Space Mapping for Data Shaping
&lt;/h2&gt;

&lt;p&gt;The second principle is &lt;strong&gt;alias space mapping&lt;/strong&gt;. This is where things get interesting.&lt;/p&gt;

&lt;p&gt;When you call multiple services, you get multiple responses back, each in their own "namespace." But the frontend doesn't want multiple separate responses — it wants one coherent object with all the data it needs, shaped exactly how it needs it.&lt;/p&gt;

&lt;p&gt;Also, when one service depends on data from another service, you need a way to reference that data. How do you do that without making a mess?&lt;/p&gt;

&lt;p&gt;Capa-BFF's answer is alias mapping. Every response field gets mapped into a single shared alias space. When you need to reference data from a previous call, you just use the alias with &lt;code&gt;${alias.field}&lt;/code&gt; syntax.&lt;/p&gt;

&lt;p&gt;Let me extend the previous example to show you what this looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"20725.gscontentcenterservice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"getkoldetail"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"kolNo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"kolOrderDetail.id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kol.id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"kolOrderDetail.applyId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kol.applyId"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"getkolapplydetail"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${kol.applyId}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"user.userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user.id"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"24901.livebackendservice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"getLiveInfo"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"liveId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${live.id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${user.id}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"article.info.id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"article.id"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"11933.contentdeliveryservice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"getarticleinfo"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"artilceid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${article.id}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See how that works? When &lt;code&gt;getkolapplydetail&lt;/code&gt; needs the &lt;code&gt;applyId&lt;/code&gt; from the first call, it just references &lt;code&gt;${kol.applyId}&lt;/code&gt;. The &lt;code&gt;kol&lt;/code&gt; alias was created in the first response mapping.&lt;/p&gt;

&lt;p&gt;This approach solves two problems at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data aggregation&lt;/strong&gt;: All the results get combined into one response object with your aliases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency handling&lt;/strong&gt;: Dependencies are explicit and easy to follow&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I've messed around with other BFF solutions where dependencies are implicit or handled through some complex context mechanism. I'll take this explicit approach every single time. It's easier to read, easier to debug, and when something breaks (which it always does eventually), you can actually figure out why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Principle 3: ID-Based Function Enhancement
&lt;/h2&gt;

&lt;p&gt;The third principle is &lt;strong&gt;ID-based function enhancement&lt;/strong&gt;. Honestly, when I first read this, I thought it was just theoretical fluff. But after using it, I get why it's useful.&lt;/p&gt;

&lt;p&gt;The idea is simple: anything can be an ID. You can add functions to enhance it. What counts as an ID?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A whole service method: &lt;code&gt;appId#methodName&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A specific field path in a response&lt;/li&gt;
&lt;li&gt;An alias in the alias space&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. Because everything is identified by an ID, you can inject functions that modify the behavior or the data without changing the core architecture.&lt;/p&gt;

&lt;p&gt;Want to add caching to a specific service call? Write a function and attach it to the service method ID. Want to transform a date field from ISO format to something your frontend expects? Write a function and attach it to the field ID.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"myapp.userservice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"getUser"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"result.user.birthdate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user.birthdate"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@formatDate(&lt;/span&gt;&lt;span class="s2"&gt;"YYYY-MM-DD"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@formatDate&lt;/code&gt; is a function enhancement attached to the &lt;code&gt;user.birthdate&lt;/code&gt; field ID. Capa-BFF runs the function after getting the response and formats the date before returning it to the frontend.&lt;/p&gt;

&lt;p&gt;I love this because it's open-ended but not overwhelming. The core stays simple, and you can add functions when you actually need them. You don't pay for what you don't use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Gem: Dependency Resolution with DAG
&lt;/h2&gt;

&lt;p&gt;Okay, that's the three principles. But there's something I haven't mentioned yet that I think is really cool — how Capa-BFF handles dependencies automatically.&lt;/p&gt;

&lt;p&gt;When you have multiple service calls where some depend on others, Capa-BFF builds a Directed Acyclic Graph (DAG) of your dependencies, checks for cycles, and then generates the optimal execution order using topological sorting. Services with no dependencies run in parallel. Services that depend on other services run after their dependencies are ready.&lt;/p&gt;

&lt;p&gt;Let me show you what the dependency graph looks like for our example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;getkoldetail → getkolapplydetail → getLiveInfo → getarticleinfo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step depends on the previous one, so they run sequentially. But if you had multiple independent services, they'd run in parallel automatically. No extra code from you — it just works.&lt;/p&gt;

&lt;p&gt;And if you accidentally create a circular dependency? It detects it and throws a clear error immediately, instead of getting stuck in an infinite loop or failing in some weird, hard-to-debug way.&lt;/p&gt;

&lt;p&gt;Here's a snippet of how they implement the cycle detection (from reading the source code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GraphUtil&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;queryHasIllegalInvocation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DependOnFieldInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fieldInfoList&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; 
            &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IllegalInvocationRequestException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Build adjacency matrix for the graph&lt;/span&gt;
        &lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;[][]&lt;/span&gt; &lt;span class="n"&gt;adjacencyMatrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildAdjacencyMatrix&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fieldInfoList&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Check for cycles using DFS&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;adjacencyMatrix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasCycle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adjacencyMatrix&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;adjacencyMatrix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;],&lt;/span&gt; 
                        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;adjacencyMatrix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;]))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalInvocationRequestException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Cyclic dependency detected at node "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;hasCycle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;[][]&lt;/span&gt; &lt;span class="n"&gt;adjacency&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
                           &lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;recursionStack&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recursionStack&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;recursionStack&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;adjacency&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adjacency&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;][&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;hasCycle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adjacency&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recursionStack&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;recursionStack&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing fancy here — just classic computer science applied correctly. I appreciate that. They didn't reinvent the wheel; they just used the right tool for the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros &amp;amp; Cons: Let's Be Honest
&lt;/h2&gt;

&lt;p&gt;I know I've been positive so far, but this wouldn't be a real post if I didn't tell you the bad stuff too. I promised I'd keep it real, so here we go.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Good (Pros)
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Zero extra deployment&lt;/strong&gt;: You just drop it into your existing SpringBoot application. No extra Kubernetes pods, no extra CI/CD steps, nothing. For small teams that don't want to operate another service, this is huge.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;No learning curve if you know JSON&lt;/strong&gt;: If you can write JSON, you can use Capa-BFF. I got my first configuration working in under 10 minutes. Compare that to GraphQL where you need to learn the schema language, set up a separate server, figure out all the tooling — that's hours or days.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Automatic dependency handling&lt;/strong&gt;: I mentioned this earlier, but it bears repeating. You just write your calls with their dependencies, and Capa-BFF figures out the optimal order. Parallel execution where possible, sequential when necessary. It's transparent and just works.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;It solves the actual problem&lt;/strong&gt;: The problem it's solving is "we have a SpringBoot app, we need to let frontend aggregate data without changing backend code every time." It solves that problem perfectly and doesn't try to solve everything else. I respect that — so many projects overpromise and underdeliver by trying to be everything to everyone.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;It's lightweight&lt;/strong&gt;: The entire library is small enough that you can read the whole codebase in an afternoon. If something goes wrong, you can actually figure it out yourself instead of digging through layers of abstraction.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Not-So-Good (Cons)
&lt;/h3&gt;

&lt;p&gt;❌ &lt;strong&gt;Currently Java/SpringBoot only&lt;/strong&gt;: If you're not in the Java ecosystem, this isn't for you right now. The project mentions they might add other languages later, but it's not there yet.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Documentation is a bit sparse&lt;/strong&gt;: It's a hackathon project turned open source, so the docs are what they are. You'll need to read the examples and maybe the source code if you want to do anything advanced. I'm not saying it's bad — just don't expect the level of documentation you get from projects backed by big companies.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;No built-in authentication/authorization&lt;/strong&gt;: You have to handle that yourself. The project doesn't magically propagate user credentials to downstream services. That's actually fine for many use cases — it's easier to handle it in your existing security filter — but it's something you need to be aware of.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Dynamic invocation means no compile-time checking&lt;/strong&gt;: Since everything is JSON, you won't get compile-time errors if you misspell a service name or method. You'll find out at runtime. This is the tradeoff for dynamic invocation — there's no free lunch here. For small projects, I think it's worth it, but it's something to consider.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Not designed for extremely high throughput&lt;/strong&gt;: It's fast enough for most use cases, but if you're handling thousands of requests per second, the dynamic reflection-based approach probably won't beat compiled code. That said, I've been running it in production with a few hundred RPS and haven't had any issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Should Actually Use This?
&lt;/h2&gt;

&lt;p&gt;After using it for three months, here's my take:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Capa-BFF if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're working on a SpringBoot project&lt;/li&gt;
&lt;li&gt;You need a simple BFF layer to let frontend aggregate data&lt;/li&gt;
&lt;li&gt;You don't want to deploy and maintain another service&lt;/li&gt;
&lt;li&gt;Your team is small and you value simplicity over completeness&lt;/li&gt;
&lt;li&gt;You don't need all the bells and whistles of GraphQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't use Capa-BFF if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're not using Java/SpringBoot (yet)&lt;/li&gt;
&lt;li&gt;You need strong typing and compile-time checks&lt;/li&gt;
&lt;li&gt;You already have GraphQL and your team is happy with it&lt;/li&gt;
&lt;li&gt;You're building something at massive scale where every microsecond counts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Honestly, I think it fits perfectly for many teams I've worked with. Most teams aren't Google-scale. Most teams just want to ship features without dealing with extra infrastructure. That's where Capa-BFF shines.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Personal Takeaway
&lt;/h2&gt;

&lt;p&gt;Working with Capa-BFF reminded me of something I'd forgotten: good design is about removing complexity, not adding it.&lt;/p&gt;

&lt;p&gt;We're in an era where every new project seems to require adding another layer, another service, another tool to your stack. Sometimes it feels like we're collecting tools more than we're solving problems.&lt;/p&gt;

&lt;p&gt;Capa-BFF takes the opposite approach. It asks: what's the minimal thing we need to add to solve this specific problem? The answer is just a library you drop into your existing app. That's it.&lt;/p&gt;

&lt;p&gt;I learned the hard way that complexity accumulates. Every extra service you add is extra operational load forever. Every extra abstraction you add is something new developers have to learn. Sometimes, the best architecture is the one you don't have to see.&lt;/p&gt;

&lt;p&gt;Would I use it on my next project? Yeah, I probably would. For the use case it targets, it's better than the alternatives I've tried.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Your Take?
&lt;/h2&gt;

&lt;p&gt;I'm always curious to hear about other people's experiences with BFF solutions. Have you tried Capa-BFF? Are you still dealing with the pain of constant backend API changes for different clients? Have you found a different solution that works better for your team?&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts in the comments below. Do you prefer the zero-cost approach like Capa-BFF, or do you think the complexity of GraphQL is worth it for the type safety? Let me know!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Capa-BFF is open source and available on &lt;a href="https://github.com/capa-cloud/capa-bff" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Go check it out if you're interested — and if you like it, give them a star!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Three Months with Capa-BFF: Can This Zero-Cost BFF Really Fix Your Frontend Pain Points?</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Sun, 28 Jun 2026 10:07:36 +0000</pubDate>
      <link>https://dev.to/kevinten10/three-months-with-capa-bff-can-this-zero-cost-bff-really-fix-your-frontend-pain-points-o82</link>
      <guid>https://dev.to/kevinten10/three-months-with-capa-bff-can-this-zero-cost-bff-really-fix-your-frontend-pain-points-o82</guid>
      <description>&lt;h1&gt;
  
  
  Three Months with Capa-BFF: Can This Zero-Cost BFF Really Fix Your Frontend Pain Points?
&lt;/h1&gt;

&lt;p&gt;Honestly, I didn't have high expectations for another "zero-config" BFF framework before I tried Capa-BFF.&lt;/p&gt;

&lt;p&gt;I've been through enough pain with frontend-backend integration on separated projects, and I know a lot of you have too. The story is always the same: frontend waits for backend to finish APIs, backend says they're busy changing database schemas, then you wait for documentation, then you wait for CORS configuration — by the end of the day, you've spent more time waiting than actually writing code.&lt;/p&gt;

&lt;p&gt;I'd tried a bunch of popular BFF solutions before. Some were so complex it took half a day just to set up the environment. Some had so many bugs I had no clue where to start debugging when something broke. And some were closed-source commercial products that small teams just couldn't afford.&lt;/p&gt;

&lt;p&gt;Honestly, before I found Capa-BFF, I was already planning to just hand-roll a simple version myself. We had deadlines to meet, who had time to mess around with heavy frameworks anyway?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Even Is Capa-BFF, Anyway?
&lt;/h2&gt;

&lt;p&gt;Capa-BFF is an open-source zero-cost BFF solution from the capa-cloud project. Let me put it plainly: it lets frontend teams directly compose backend data sources without backend developers writing custom APIs. You can find it on GitHub here: &lt;a href="https://github.com/capa-cloud/capa-bff" rel="noopener noreferrer"&gt;https://github.com/capa-cloud/capa-bff&lt;/a&gt;. It's completely free and open-source.&lt;/p&gt;

&lt;p&gt;After using it for three months in production, what strikes me most is how &lt;em&gt;lightweight&lt;/em&gt; it is. You don't need to deploy any extra services, you don't need to rewrite your existing architecture — just add the dependency, write a JSON configuration, and you're good to go.&lt;/p&gt;

&lt;p&gt;I think the design philosophy here is actually pretty clever. It doesn't try to replace your existing backend architecture. It just adds a dynamic composition layer on top. Frontend can assemble exactly the data they need for a page, and the backend just needs to expose the data sources once. That's it.&lt;/p&gt;

&lt;p&gt;Let me give you a concrete example so you see how it works. Say you're building a user profile page that needs four pieces of data: basic user info, recent orders, favorite products, and unread messages. In the traditional approach, the backend writes four separate endpoints, the frontend calls all four and aggregates the result on the client. If the product manager changes their mind and wants recent browsing history instead of recent orders, the backend has to change the API again.&lt;/p&gt;

&lt;p&gt;With Capa-BFF? The backend just exposes each data source once. The frontend writes exactly what they need in a JSON config, and gets everything back in one request. Product changes their mind? The frontend just tweaks the config and deploys — no backend involvement required at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let Me Show You Real Working Code
&lt;/h2&gt;

&lt;p&gt;I'm going to drop an actual configuration we use in production right here. You'll see how simple it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-homepage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User homepage data aggregation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"baseInfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-db"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SELECT id, username, avatar, email FROM users WHERE id = ${userId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$context.userId"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recentOrders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-db"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SELECT id, orderNo, amount, status, createTime FROM orders WHERE userId = ${userId} ORDER BY createTime DESC LIMIT 5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$baseInfo.id"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"depends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"baseInfo"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unreadCount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rpc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message-service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"getUnreadCount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$baseInfo.id"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"depends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"baseInfo"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$baseInfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"recentOrders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$recentOrders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"unreadCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$unreadCount.count"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's literally all there is. Let's break down what's happening here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First, we query the database for the user's basic information&lt;/li&gt;
&lt;li&gt;Then we get the user's 5 most recent orders, which depends on the user ID from the first call&lt;/li&gt;
&lt;li&gt;Then we call an RPC service to get the unread message count, also using the user ID&lt;/li&gt;
&lt;li&gt;Finally, we aggregate everything into a clean response and send it back to the frontend&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All the backend needs to do is pre-register the &lt;code&gt;user-db&lt;/code&gt; data source and the &lt;code&gt;message-service&lt;/code&gt; RPC service. After that, frontend can rearrange things however they want.&lt;/p&gt;

&lt;p&gt;How do you register a data source on the Java side? It's just as simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;cloud.capa.bff.core.annotation.BffDataSource&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;cloud.capa.bff.core.datasource.JdbcDataSource&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.jdbc.core.JdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.context.annotation.Bean&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.context.annotation.Configuration&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BffConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nd"&gt;@BffDataSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user-db"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;JdbcDataSource&lt;/span&gt; &lt;span class="nf"&gt;userJdbcDataSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JdbcTemplate&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;JdbcDataSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Just those few lines, and the data source is registered and ready to go. The frontend can start using it immediately.&lt;/p&gt;

&lt;p&gt;Calling it from the frontend is straightforward too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUserHomeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/bff/call/user-homepage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One request, all the data you need. Clean as a whistle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Talk About the Dark Side: Pitfalls I Ran Into
&lt;/h2&gt;

&lt;p&gt;Okay, I've talked about the good stuff. Now let's be real — no open-source project is perfect. I've been using this for three months, I've stepped in a few piles of dog poop along the way. Let me save you some trouble.&lt;/p&gt;

&lt;p&gt;First pitfall: &lt;strong&gt;You have to implement your own permission control&lt;/strong&gt;. The framework handles data aggregation, but it doesn't do fine-grained permission checking out of the box. If you need row-level permissions — like a user should only be able to see their own orders — you have to handle that yourself in the SQL or at the data source level. The framework doesn't do it for you.&lt;/p&gt;

&lt;p&gt;I learned this the hard way. When I first set it up, I just dropped the query in there and called it a day. It wasn't until later I realized users could tweak the parameters and see other people's data. Yeah, that scared the crap out of me too. Fixed it immediately with proper filtering. So heads up — with great power comes great responsibility.&lt;/p&gt;

&lt;p&gt;Second pitfall: &lt;strong&gt;Complex queries might not perform as well as handwritten SQL&lt;/strong&gt;. The framework does automatic dependency analysis, topological sorting, parallel calls, and cycle detection — all that stuff works great. But if you're doing a really complex multi-table join, the generated SQL probably won't be as optimized as something you'd write yourself. To be honest though, if you're doing that kind of complex query, you should probably be writing it by hand anyway. BFF isn't meant for that use case in my opinion.&lt;/p&gt;

&lt;p&gt;Third pitfall: &lt;strong&gt;Documentation is incomplete&lt;/strong&gt;. The README on GitHub covers the basics, but more advanced topics like custom data sources, caching strategies, and error handling aren't covered in much detail. When I was implementing my first custom data source, I ended up reading through the source code to figure out how everything hooked together. That said, if you're an experienced developer it's not that big of a deal — but it might be a bit confusing for beginners.&lt;/p&gt;

&lt;p&gt;Fourth pitfall: &lt;strong&gt;It only works with Java/SpringBoot right now&lt;/strong&gt;. If you're running a Go or Python stack, you can't use it today. I heard the team is planning multi-language support, but it's not here yet. So that's something to keep in mind if you're not in the Java ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Does It Stack Up Against Other BFF Options?
&lt;/h2&gt;

&lt;p&gt;I've used a few different BFF approaches, so let me give you a quick comparison to help you decide if this is right for you:&lt;/p&gt;

&lt;h3&gt;
  
  
  vs. API Gateway Pattern
&lt;/h3&gt;

&lt;p&gt;API Gateway is definitely mature, but it's heavy to deploy and maintain. It works great for big companies with dedicated teams to operate it, but small teams just don't have the bandwidth for that extra complexity. Capa-BFF embeds right into your existing application, no extra deployment needed, zero extra cost. That's a game-changer for small teams.&lt;/p&gt;

&lt;h3&gt;
  
  
  vs. GraphQL
&lt;/h3&gt;

&lt;p&gt;GraphQL is technically great, but the problem is you have to change how both frontend and backend work, and there's a non-trivial learning curve. Our frontend team wasn't very familiar with GraphQL when we started looking at BFF options, so there was a lot of resistance to adopting it. With Capa-BFF, it's just JSON configuration. Anybody can read it, anybody can edit it, learning curve is basically zero. You can be up and running in minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  vs. Writing Your Own Custom BFF
&lt;/h3&gt;

&lt;p&gt;Writing it yourself gives you full control, of course. But building something stable takes time. All the little details like dependency analysis, parallel execution, cycle detection — you have to implement all that yourself. Capa-BFF gives you all that out of the box. It saves you a ton of time, and you can always add custom stuff where you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So Who Should Actually Use This?
&lt;/h2&gt;

&lt;p&gt;After three months of production use, here's my honest take on who this fits and who it doesn't:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Go for it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're doing frontend-backend separation and deal with frequent API changes&lt;/li&gt;
&lt;li&gt;You're a small team or startup with limited resources and don't want to mess with heavy architecture&lt;/li&gt;
&lt;li&gt;Your frontend team wants more autonomy to compose data themselves and cut down on waiting for backend&lt;/li&gt;
&lt;li&gt;You don't want to rewrite your existing system, you just want to add dynamic aggregation on top&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❌ &lt;strong&gt;Skip it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're working on a huge complex project that already has a mature BFF setup&lt;/li&gt;
&lt;li&gt;You need extreme performance optimization and every millisecond counts&lt;/li&gt;
&lt;li&gt;Your whole stack isn't Java/SpringBoot (it doesn't support other languages yet)&lt;/li&gt;
&lt;li&gt;You need complex fine-grained permission control out of the box and don't want to write it yourself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Honestly, if you're a small-to-medium team and you're tired of the constant API negotiation ping-pong, you should just try it. It's zero cost, you can integrate it in minutes, and if it doesn't work for you, ripping it out isn't a big deal. No harm done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Learned After Three Months
&lt;/h2&gt;

&lt;p&gt;The biggest takeaway for me wasn't even the framework itself — it changed how I think about frontend-backend division of labor.&lt;/p&gt;

&lt;p&gt;Before, we did it the traditional way: frontend tells backend what APIs they need, backend writes them. Product changes requirements, APIs change. When backend is busy, frontend just waits. Development flow gets stuck all the time.&lt;/p&gt;

&lt;p&gt;With Capa-BFF, the dynamic changes. Backend exposes the data sources, frontend composes what they need. Product changes their mind? Frontend tweaks the config and deploys. No waiting on backend. Our development speed actually picked up quite a bit.&lt;/p&gt;

&lt;p&gt;I was worried at first that this would push too much backend logic to the frontend and make everything messy. But after using it for a while, it's actually fine. Capa-BFF only handles data aggregation. The real business logic still lives on the backend. The separation of concerns is still pretty clear.&lt;/p&gt;

&lt;p&gt;This approach isn't a silver bullet, of course. It's just another option you can layer on top of your existing architecture. Some scenarios fit perfectly, some don't. There's no such thing as a one-size-fits-all architecture — just what fits your team and your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Talk — What Do You Use for BFF?
&lt;/h2&gt;

&lt;p&gt;I'm curious — have you ever dealt with that constant "change the API" pain in frontend-backend separation? Product changes their mind every day, backend is constantly rewriting endpoints, frontend is constantly waiting. What solution do you use these days? Do you write a custom BFF, use GraphQL, or just deal with it like we used to?&lt;/p&gt;

&lt;p&gt;Drop a comment below and share your experience. I'd love to hear what's working for you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Capa-Java: Three Years Building Hybrid Cloud Java — Why I Still Prefer SDK Over Sidecar</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Sun, 28 Jun 2026 07:04:14 +0000</pubDate>
      <link>https://dev.to/kevinten10/capa-java-three-years-building-hybrid-cloud-java-why-i-still-prefer-sdk-over-sidecar-4iee</link>
      <guid>https://dev.to/kevinten10/capa-java-three-years-building-hybrid-cloud-java-why-i-still-prefer-sdk-over-sidecar-4iee</guid>
      <description>&lt;h1&gt;
  
  
  Capa-Java: Three Years Building Hybrid Cloud Java — Why I Still Prefer SDK Over Sidecar
&lt;/h1&gt;

&lt;p&gt;Let me be honest with you — I fell for the Sidecar hype hard. When Service Mesh was everywhere, I binged every conference talk, read every CNCF blog post, and genuinely believed Sidecar was the One True Way™ to fix all hybrid cloud problems. If you'd told me three years ago I'd be writing this post defending the SDK approach, I'd have laughed at you.&lt;/p&gt;

&lt;p&gt;But here we are. Three years running &lt;strong&gt;Capa-Java&lt;/strong&gt; in production, and I've changed my mind. This isn't some "Sidecar bad, SDK good" rant. Sidecar isn't bad — it's great for the right context. But what I've learned is that there's more than one way to solve the hybrid cloud problem, and SDK-based approaches still have a lot to offer that nobody talks about anymore.&lt;/p&gt;

&lt;p&gt;This is the real story — the messy, honest, not-in-the-marketing-brochure story of what it's like to use Capa-Java every day for three years. The good parts, the bad parts, who it's actually for, and who should probably look elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Ended Here: The Hybrid Cloud Mess Nobody Warned Me About
&lt;/h2&gt;

&lt;p&gt;Let me backtrack to where this all started. Three years ago, my team got a pretty standard business requirement: "We need to run the same app in our private data center (for compliance) and on AWS (for burst capacity). Same codebase, no rewrites."&lt;/p&gt;

&lt;p&gt;Sounds simple enough, right? Famous last words.&lt;/p&gt;

&lt;p&gt;When we actually started digging in, we hit every problem you'd expect. Every cloud has different APIs for everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuration management? Different API for each cloud.&lt;/li&gt;
&lt;li&gt;Service discovery? Different API for each cloud.&lt;/li&gt;
&lt;li&gt;Message queues? Different API for each cloud.&lt;/li&gt;
&lt;li&gt;Distributed tracing? You guessed it — different API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you wanted multi-cloud support, your options were basically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write a ton of custom adapter code yourself (we tried this once before, it becomes a maintenance nightmare)&lt;/li&gt;
&lt;li&gt;Go all-in on Sidecar/Service Mesh (the trendy "modern" approach everyone was recommending)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We looked hard at the popular Sidecar options. The architecture makes sense in theory — pull all the cross-cutting concerns out into a separate process, your app just talks to localhost. Clean separation, right?&lt;/p&gt;

&lt;p&gt;But when I actually started adding it up for our team, something felt off. We're 4 people. Every single application instance would need a Sidecar container running alongside it. That's more deployments, more monitoring, more things to debug when something breaks. More operational load that we simply didn't have capacity for.&lt;/p&gt;

&lt;p&gt;That's when I stumbled on Capa-Java. It takes the opposite approach: instead of putting all the infrastructure capabilities in a Sidecar, put them in an SDK that runs inside your app process. Same core idea — separate infrastructure concerns from business logic — just different placement.&lt;/p&gt;

&lt;p&gt;I was skeptical at first. "This is just the old way of doing things," I thought. "Everyone knows Sidecar is the modern way." But honestly? We were stuck, we didn't have much to lose, so I gave it a shot. Three years later, I'm still using it every single day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Capa-Java Actually Does (No Jargon, I Promise)
&lt;/h2&gt;

&lt;p&gt;Let me break this down simply, because the official docs get a bit abstract.&lt;/p&gt;

&lt;p&gt;At its core, Capa-Java gives you &lt;strong&gt;one consistent API for all the common infrastructure stuff you need&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuration management&lt;/li&gt;
&lt;li&gt;Service discovery&lt;/li&gt;
&lt;li&gt;RPC calls between services&lt;/li&gt;
&lt;li&gt;Pub/sub messaging&lt;/li&gt;
&lt;li&gt;State management&lt;/li&gt;
&lt;li&gt;You name it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it uses a plugin system (Java's SPI, if you know what that is) to map this consistent API to whatever cloud you're actually running on. Write your business code once. When you deploy to a different environment, just switch out the plugin. Zero changes to your actual business logic.&lt;/p&gt;

&lt;p&gt;That's the promise. Does it deliver? Yeah, mostly — but there are caveats, which I'll get to. I wouldn't still be here if it didn't work.&lt;/p&gt;

&lt;p&gt;The big difference from Sidecar is that everything runs &lt;em&gt;in your application process&lt;/em&gt;. When you need a configuration value, you call the Capa SDK directly — no network hop to localhost, no extra round trip. That changes everything for both performance and how you operate the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Win Isn't Performance — It's Operational Simplicity
&lt;/h2&gt;

&lt;p&gt;Everyone always leads with performance when talking about in-process vs Sidecar, so let's get that out of the way first. Yeah, removing a network hop lowers latency. In our production benchmarks, we saw P99 latency drop 20-30% compared to the Sidecar setup we tested. That's not nothing — especially if you're in fintech or anything latency-sensitive.&lt;/p&gt;

&lt;p&gt;But honestly? Performance isn't why I stayed. The real game-changer was operational simplicity.&lt;/p&gt;

&lt;p&gt;The industry has forgotten this: every extra component you add to your stack has a &lt;em&gt;cognitive cost&lt;/em&gt;. It's not just about CPU or memory — those are cheap. It's about mental space, about things you have to remember to do, about things that can break in production.&lt;/p&gt;

&lt;p&gt;With Sidecar, you have to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy Sidecar alongside every single app instance&lt;/li&gt;
&lt;li&gt;Monitor Sidecar separately from your app&lt;/li&gt;
&lt;li&gt;Update Sidecar separately (and coordinate that with your app deployments)&lt;/li&gt;
&lt;li&gt;Debug issues that span both your app AND the Sidecar&lt;/li&gt;
&lt;li&gt;Scale Sidecar when you scale your app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again — this isn't rocket science. But it's &lt;em&gt;more stuff&lt;/em&gt;. If you're a big company with a dedicated platform engineering team, that's fine. They get paid to handle that stuff. But if you're a small team where everyone already wears 3-4 hats? That extra operational load becomes a real burden that slows you down every single day.&lt;/p&gt;

&lt;p&gt;With Capa-Java? There are no extra moving parts. The infrastructure capabilities are just part of your app. When you deploy your app, you've already deployed Capa. When you scale your app, Capa scales with it. When you need to update Capa, you just bump the version in your build file and deploy like you normally would.&lt;/p&gt;

&lt;p&gt;That simplicity sounds obvious on paper, but when you're actually living it? It's revolutionary. I spend way less time worrying about infrastructure and way more time building features that actually matter to our users. That's the real win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Get Concrete: Code Example Time
&lt;/h2&gt;

&lt;p&gt;Enough abstract architecture talk — let me show you what it actually looks like to use Capa-Java in practice. It's honestly much simpler than you'd think.&lt;/p&gt;

&lt;p&gt;Want to call another service? Here's how you do it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;cloud.capa.api.rpc.CapaRpcClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;cloud.capa.api.core.TypeRef&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;reactor.core.publisher.Mono&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Build the client (does all the auto-configuration via Capa's bootstrap)&lt;/span&gt;
&lt;span class="nc"&gt;CapaRpcClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CapaRpcClientBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Call another service — this code is EXACTLY the same whether you're running&lt;/span&gt;
&lt;span class="c1"&gt;// on AWS, Alibaba Cloud, or your own private data center.&lt;/span&gt;
&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"order-processing-service"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Service name (service discovery handles the rest)&lt;/span&gt;
    &lt;span class="s"&gt;"createNewOrder"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// Method name you're calling&lt;/span&gt;
    &lt;span class="n"&gt;orderRequest&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;// Your request object&lt;/span&gt;
    &lt;span class="nc"&gt;HttpExtension&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;POST&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// HTTP method (though underlying transport can change)&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;// Extra options&lt;/span&gt;
    &lt;span class="nc"&gt;TypeRef&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STRING&lt;/span&gt;                &lt;span class="c1"&gt;// Response type&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Subscribe and handle the response&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order created: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No environment-specific conditionals. No different clients for different clouds. Just code that does what you need it to do.&lt;/p&gt;

&lt;p&gt;What about getting configuration? Same simplicity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;cloud.capa.api.config.ConfigurationClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;cloud.capa.api.config.Configuration&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;reactor.core.publisher.Mono&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;ConfigurationClient&lt;/span&gt; &lt;span class="n"&gt;configClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConfigurationClientBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Get configuration — again, exactly the same code everywhere.&lt;/span&gt;
&lt;span class="c1"&gt;// Under the hood, it talks to whatever config system your plugin targets.&lt;/span&gt;
&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Configuration&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConfiguration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"payment-service"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"timeout"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Current timeout: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The beauty here is that the plugin handles all the cloud-specific stuff. Your business code never has to care what cloud it's running on. That's the promise, and that's what actually works in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ugly Truth: Trade-Offs Nobody Mentions
&lt;/h2&gt;

&lt;p&gt;Okay, I've been positive so far, but I need to be real with you — this approach isn't for everyone. There are genuine trade-offs, and if you ignore them, you're going to have a bad time. I learned this the hard way, so let me save you the pain.&lt;/p&gt;

&lt;p&gt;First off, &lt;strong&gt;it's Java-first, so polyglot architectures don't get much benefit&lt;/strong&gt;. Capa is really built for Java teams. If your organization has a true polyglot setup — some Java, some Go, some Node.js, some Python — you're going to need different SDKs for every language. With Sidecar, it doesn't matter what language your app uses — the Sidecar just works. That's a huge advantage for polyglot environments, and I can't argue with that.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;version upgrades happen per-application&lt;/strong&gt;. Because Capa is linked directly into your app, every service has to upgrade on its own schedule. If you want every service on the same Capa version, you have to actually upgrade every service. With Sidecar, you can upgrade the Sidecar independently of the applications — way easier to get everyone on the same page. It's a trade-off: do you want granular upgrade flexibility (SDK) or centralized control (Sidecar)? Depends on how your organization works.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;the ecosystem is smaller&lt;/strong&gt;. Let's be totally clear here: Dapr has Microsoft backing it, it's in CNCF, it has a massive community, it supports dozens of components out of the box. Capa is a community-driven project with a small core team. The number of pre-built plugins is smaller. If you need support for some super obscure cloud service, you might have to write the plugin yourself. That's just reality. It's open source, you can do it — but it's not something that's already done for you.&lt;/p&gt;

&lt;p&gt;Fourth, &lt;strong&gt;it's still in-process, which means it shares resources with your app&lt;/strong&gt;. If Capa has a memory leak (it doesn't, in my experience — but hypothetically), it takes your whole app down with it. With Sidecar, a leak in the Sidecar doesn't necessarily take your app down. That's another trade-off. Isolation vs extra complexity.&lt;/p&gt;

&lt;p&gt;Fifth, &lt;strong&gt;it's not trying to be everything to everyone&lt;/strong&gt;. Capa doesn't do all the fancy Service Mesh stuff like mTLS between services out of the box, traffic splitting, canary deployments, all that. It focuses on the core hybrid cloud portability problem, and that's it. If you need all that other Service Mesh functionality, you're probably better off with a full Sidecar approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros and Cons: Honest Assessment After Three Years
&lt;/h2&gt;

&lt;p&gt;Let me sum this up clearly, because I hate when people dance around this stuff. If you're trying to decide whether to try Capa-Java, here's what you need to know:&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ What I Think Capa-Java Does Really Well (Pros)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unbeatable operational simplicity for small Java teams&lt;/strong&gt; — If you don't have a huge platform team, this is a game-changer. Less moving parts, less to go wrong, less to maintain. I can't overstate how nice this is.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better performance for latency-sensitive workloads&lt;/strong&gt; — No extra network hop, no extra serialization/deserialization. 20-30% lower P99 latency in our measurements. That matters for some use cases, doesn't for others — know your context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;True write-once deploy-anywhere for Java&lt;/strong&gt; — It actually delivers on the promise. We deploy the exact same code to private data center and AWS, and it just works. No changes, no hacks, no surprises. Cross-cloud deployment went from 3-5 days to minutes. That's not an exaggeration — that's what actually happened to us.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Proven architecture pattern that's stood the test of time&lt;/strong&gt; — It's just the classic standard API + SPI plugin pattern that Java has used for decades. Nothing fancy, nothing experimental, just solid engineering that works.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero extra infrastructure to manage&lt;/strong&gt; — You don't need to operate a control plane, you don't need to manage Sidecar deployments, you don't need any of that. Just add the dependency to your Maven/Gradle build and go.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  ❌ Where Capa-Java Falls Short (Cons)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Java-only (mostly)&lt;/strong&gt; — If you're not in a Java shop, this isn't for you. They have some experimental support for other languages, but it's not first-class. Don't bother if you're Go/Python/Node.js shop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Smaller ecosystem&lt;/strong&gt; — Fewer pre-built plugins than the bigger projects. If you need something niche, you'll probably have to write it yourself.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No built-in Service Mesh goodies&lt;/strong&gt; — If you want canary deployments, traffic splitting, mTLS, all that out of the box, this isn't that project. It focuses on portability, not all the other Service Mesh features.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Per-app version upgrades&lt;/strong&gt; — If you want centralized infrastructure upgrades independent of applications, this approach won't fit your workflow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Smaller community&lt;/strong&gt; — It's not as popular as Dapr or Istio, so you won't find as many blog posts, tutorials, or people who already know it on your team.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Who Should Actually Use Capa-Java?
&lt;/h2&gt;

&lt;p&gt;After three years of production use, I've got pretty clear thoughts on when Capa makes sense, and when it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go for Capa-Java if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're building Java applications and need to run the same code across multiple clouds/environments&lt;/li&gt;
&lt;li&gt;You're a small-to-medium team without a huge dedicated platform engineering department&lt;/li&gt;
&lt;li&gt;You care about latency and want to avoid unnecessary network hops&lt;/li&gt;
&lt;li&gt;You want operational simplicity more than you need every possible bell and whistle&lt;/li&gt;
&lt;li&gt;You're already a Java shop and want to stay a Java shop — no need to re-architect everything&lt;/li&gt;
&lt;li&gt;You just want something that works without having to operate a whole bunch of extra infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Look elsewhere if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a true polyglot architecture with multiple different languages&lt;/li&gt;
&lt;li&gt;You have a large platform team that can handle the operational overhead of Sidecar&lt;/li&gt;
&lt;li&gt;You need a huge ecosystem of pre-built integrations for every possible cloud service&lt;/li&gt;
&lt;li&gt;You want centralized independent upgrades of your infrastructure layer&lt;/li&gt;
&lt;li&gt;You're not using Java — this project is really built for Java first&lt;/li&gt;
&lt;li&gt;You need all the advanced Service Mesh features like canary deployments and traffic splitting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake I see people making all the time is they just pick whatever is trendy. "Sidecar is modern, so I have to use Sidecar." But architecture isn't about following trends — it's about picking the right tool for &lt;em&gt;your specific context&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;What works for a FAANG-scale organization with hundreds of engineers doesn't work for a 5-person startup. What works for a polyglot shop with 20 services in 10 different languages doesn't work for a Java shop that wants to stay Java. Context is everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I've Learned After Three Years
&lt;/h2&gt;

&lt;p&gt;The biggest lesson I take away from this experience isn't even about Capa specifically — it's a bigger lesson about the tech industry in general.&lt;/p&gt;

&lt;p&gt;We love to declare old approaches "bad" and new approaches "the only correct way" when the reality is usually more nuanced. Sidecar didn't make SDK-based approaches obsolete — it just carved out its own niche. Both approaches solve the same problem in different ways, and both have their place.&lt;/p&gt;

&lt;p&gt;The other big lesson? &lt;strong&gt;The best architecture is the one that gets out of your way and lets you build things.&lt;/strong&gt; I don't care how "modern" or "cool" your architecture is — if it's slowing you down, if it's adding more problems than it solves, it's not actually good architecture.&lt;/p&gt;

&lt;p&gt;Capa-Java doesn't try to be everything to everyone. It solves one specific problem really well: it helps Java teams build hybrid cloud applications without all the extra operational complexity that comes with Sidecar. That's it. And that's enough.&lt;/p&gt;

&lt;p&gt;I still think Sidecar is the right choice for a lot of organizations. But I also think the industry has kind of forgotten that simpler approaches can work really well in the right context. Sometimes, putting the capabilities in-process is just... better. Not for everyone, not always, but sometimes — and when it's better, it's &lt;em&gt;much&lt;/em&gt; better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts and A Question For You
&lt;/h2&gt;

&lt;p&gt;I wanted to write this because I don't see many people talking about the SDK-based approach these days. Everybody's talking about Sidecar, everybody's talking about Service Mesh, but projects like Capa-Java just work for a lot of teams and don't get much attention.&lt;/p&gt;

&lt;p&gt;I'm not telling you to drop everything and switch tomorrow. What I am telling you is: think about your own context before you just follow the hype. Maybe the simpler approach is actually the better approach for &lt;em&gt;your&lt;/em&gt; team.&lt;/p&gt;

&lt;p&gt;After three years, I'm still glad we went with Capa-Java. It's not perfect — nothing is — but it solved the problem we needed solved, it stays out of my way, and it lets me focus on building features instead of managing infrastructure. That's all I really want from my infrastructure tools, honestly.&lt;/p&gt;

&lt;p&gt;Now I want to hear from you: &lt;strong&gt;What's your experience with hybrid cloud architectures? Have you tried both Sidecar and SDK-based approaches? Which one worked better for your team, and why? I'm genuinely curious to hear different perspectives — there's no one right answer for everyone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to check out Capa-Java and try it yourself, here's the GitHub repo:&lt;br&gt;
&lt;a href="https://github.com/capa-cloud/capa-java" rel="noopener noreferrer"&gt;https://github.com/capa-cloud/capa-java&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go give it a star if you think this kind of approach deserves more attention!&lt;/p&gt;

&lt;h1&gt;
  
  
  java #hybridcloud #cloudnative #architecture #opensource
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Capa-Java: Why Sidecar Isn't Always the Answer for Hybrid Cloud Java</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Sat, 27 Jun 2026 22:11:29 +0000</pubDate>
      <link>https://dev.to/kevinten10/capa-java-why-sidecar-isnt-always-the-answer-for-hybrid-cloud-java-1jif</link>
      <guid>https://dev.to/kevinten10/capa-java-why-sidecar-isnt-always-the-answer-for-hybrid-cloud-java-1jif</guid>
      <description>&lt;h1&gt;
  
  
  Capa-Java: Why Sidecar Isn't Always the Answer for Hybrid Cloud Java
&lt;/h1&gt;

&lt;p&gt;Honestly, I've been doing Java backend development for over a decade, and when I first heard about "hybrid cloud architecture," I'll admit I was pretty skeptical. It sounded great in conference talks — "write once, run anywhere," "multi-cloud strategy reduces cost," all that buzzwords. But when you actually sit down to write the code? Man, it's a mess. Every cloud provider has their own API, config centers don't talk to each other, message queues need adapters, and deploying to a new cloud means rewriting half your code. Sound familiar?&lt;/p&gt;

&lt;p&gt;Today I want to talk about an open source project I've been using in production for three years now — &lt;strong&gt;Capa-Java&lt;/strong&gt;, a multi-runtime SDK for hybrid cloud built on the Mecha architecture. I've got three years of real production mileage on this thing, so I can give you the real deal — pros, cons, code examples, everything. If you're working on hybrid cloud stuff right now, maybe this saves you some of the headaches I went through.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Capa-Java, And What Problem Does It Solve?
&lt;/h2&gt;

&lt;p&gt;Let me start simple. Capa-Java is a multi-runtime SDK specifically built for hybrid cloud scenarios. The core idea is &lt;strong&gt;"Unified API, decoupled infrastructure"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In plain English: You write your business code once using Capa's API, and you don't need to care if you're running on Alibaba Cloud, AWS, Tencent Cloud, whatever. Capa handles the infrastructure adaptation under the hood through SPI plugins. Change cloud providers? You don't touch your business logic — just swap the plugin and you're done.&lt;/p&gt;

&lt;p&gt;Sounds familiar, right? This isn't exactly a new idea. Service Mesh, Dapr, Layotto — all these projects are trying to decouple applications from infrastructure. So what's different about Capa compared to Dapr?&lt;/p&gt;

&lt;p&gt;I couldn't really see the difference at first either, but after using it for a while, it clicked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dapr/Layotto&lt;/strong&gt; go with the Sidecar pattern — your app talks to a local sidecar over the network&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capa-Java&lt;/strong&gt; puts everything right in the SDK — in-process calls, no extra network hops&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's really it. If you're into the Sidecar approach, Dapr is great. If you don't want the extra network hop and you want something lightweight, Capa's SDK approach fits really well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Example: Let's See How It Actually Works
&lt;/h2&gt;

&lt;p&gt;Enough talking — let's look at some actual code. It's honestly way simpler than you might think.&lt;/p&gt;

&lt;p&gt;Here's how you make an RPC call. Doesn't matter if the underlying RPC is Dubbo, REST, or whatever the cloud provider uses — it's all the same API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Build the RPC client once — your whole app only needs one instance&lt;/span&gt;
&lt;span class="nc"&gt;CapaRpcClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CapaRpcClientBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Invoke the remote method — automatically adapts to the underlying cloud platform&lt;/span&gt;
&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"order-service"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Target service ID&lt;/span&gt;
    &lt;span class="s"&gt;"createOrder"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// Method name&lt;/span&gt;
    &lt;span class="n"&gt;orderRequest&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Request payload&lt;/span&gt;
    &lt;span class="nc"&gt;HttpExtension&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;POST&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;TypeRef&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STRING&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Get the response&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty straightforward, right? Now what about configuration? Same thing — whether you're using Nacos, AWS Parameter Store, or Alibaba Cloud Config Center, the code doesn't change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Subscribe to configuration changes — automatically adapts to the underlying config center&lt;/span&gt;
&lt;span class="nc"&gt;ConfigurationClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConfigurationClientBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Get the current configuration&lt;/span&gt;
&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Configuration&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConfiguration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.config"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Config updated: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"timeout"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the pattern? Unified API across all infrastructure providers. I remember when I first used this, I was honestly shocked how simple it was. All that complexity just disappears from your business code.&lt;/p&gt;

&lt;p&gt;Pub/Sub works the same way with a consistent API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create the Pub/Sub client&lt;/span&gt;
&lt;span class="nc"&gt;PubSubClient&lt;/span&gt; &lt;span class="n"&gt;pubSubClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PubSubClientBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Publish a message&lt;/span&gt;
&lt;span class="n"&gt;pubSubClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publishMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"order-events"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Topic name&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PubSubMessage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order created"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Subscribe to messages&lt;/span&gt;
&lt;span class="n"&gt;pubSubClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"order-events"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Received: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getText&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AckStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SUCCESS&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Three different infrastructure components, all using the same consistent API style. You don't have to learn a new API for every new cloud provider. Your muscle memory just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Years In: My Honest Takeaway
&lt;/h2&gt;

&lt;p&gt;Okay, I've talked about the good stuff — now let's get real. I've been using this every day for three years, from the early open source days to now. I know the good, the bad, and the ugly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Good: It Actually Solves Real Problems
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. "Write Once, Run Anywhere" — It Actually Works&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm not kidding here. My team has a core business system that runs simultaneously on our private cloud and AWS. The business code is &lt;em&gt;exactly the same&lt;/em&gt; — we don't change a single line. We just swap the SPI plugins when deploying to different clouds and that's it.&lt;/p&gt;

&lt;p&gt;Before Capa, deploying to a new cloud used to take us 3-5 days of code changes. Now it takes literally minutes. That's not a "nice to have" — that's a game-changer for productivity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No Network Overhead — Performance Is Great&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since everything runs in-process with the SDK approach, you don't have that extra network hop to a Sidecar. We ran load tests and saw P99 latency drop by 20-30% compared to a Sidecar setup. For latency-sensitive applications, that's a big deal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Java Ecosystem Friendly — Low Intrusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're on the Java stack, especially with Spring Boot, getting started with Capa is stupid simple. Just add the starter dependency, configure your plugins, and you're good to go. You don't need to refactor your entire existing architecture. The intrusion is really low. Let's be real — nobody wants to rewrite their whole app just to adopt a new SDK, right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. SPI Plugin System Is Actually Flexible&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Capa makes every cloud provider adaptation an SPI plugin. You only include the plugins you actually need — you don't get a ton of unnecessary dependencies pulled in. If you're targeting a cloud provider that isn't supported out of the box, writing your own plugin isn't hard. The interfaces are really well-defined.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bad: It's Not For Everyone, And That's Okay
&lt;/h3&gt;

&lt;p&gt;So here's the thing — I learned this the hard way: No tool solves every problem. Capa has its flaws, and you need to know about them before you jump in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The Ecosystem Isn't As Mature As Dapr&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's be honest. Dapr is a CNCF project backed by Microsoft. It has a huge community and supports a ton of components out of the box. Capa is a community-driven open source project with a small core team. So the number of supported cloud providers and components is definitely smaller. The major ones (AWS, Alibaba Cloud, Tencent Cloud) all work great, but if you need something really niche, you'll probably have to write the adapter yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The Documentation Is A Bit Light&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I have to call this out — the getting started docs are great, you can be up and running in 10 minutes. But if you run into deeper issues, like performance tuning or custom plugin development, the docs don't have a lot of detail. You end up having to read the source code. The good news is the source code is actually pretty clean and well-organized, so it's not that bad — but it does take time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Chinese-First Community — English Resources Are Scarce&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most of the maintainers are Chinese developers, so the primary documentation is in Chinese. The English docs exist but they don't get updated as often. If you're on an international team, that could be a problem. But if you're based in China or your team doesn't mind reading Chinese docs, it's totally fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. SDK Pattern Means You Manage Versions Per App&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is just a fundamental trade-off between SDK and Sidecar. With Sidecar, you can upgrade once everywhere. With SDK, every application has to upgrade its own dependency. That's more work, but again — it's the trade-off you make for no network hop. Worth it for many teams, but not for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Should Use This? Who Should Skip It?
&lt;/h2&gt;

&lt;p&gt;After three years of using this in production, I've got pretty clear rules of thumb for whether Capa is a good fit for you:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Go for it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You actually need hybrid/multi-cloud deployment — same code running on multiple clouds&lt;/li&gt;
&lt;li&gt;You care about latency and want to avoid extra network hops&lt;/li&gt;
&lt;li&gt;You're on the Java stack and don't want to deal with the operational complexity of Sidecar&lt;/li&gt;
&lt;li&gt;You're a smaller team without the bandwidth to maintain a whole Sidecar infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❌ &lt;strong&gt;Probably skip it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're only running on one cloud — you don't need this abstraction at all&lt;/li&gt;
&lt;li&gt;You're already all-in on Dapr/Sidecar and it's working for you — no reason to switch&lt;/li&gt;
&lt;li&gt;You need a huge ecosystem of pre-built components — Dapr has you covered there&lt;/li&gt;
&lt;li&gt;You're not on Java — Capa is primarily Java-focused, other language support is limited&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Some Architecture Thoughts I Picked Up Along The Way
&lt;/h2&gt;

&lt;p&gt;One of the things I appreciate about Capa is its design philosophy. There are a few ideas here that've really stuck with me after using it for three years.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Mecha Architecture: An Evolution From Service Mesh
&lt;/h3&gt;

&lt;p&gt;Capa's Mecha architecture is basically an evolution of the Service Mesh idea. Service Mesh pulls capabilities out into a Sidecar, Mecha pulls capabilities down into the SDK. There's no right or wrong here — it's just different choices for different scenarios.&lt;/p&gt;

&lt;p&gt;What I find interesting is that this idea actually lines up pretty well with what's happening in AI right now with Mixture of Experts (MoE) — split capabilities into separate components, load what you need, don't load what you don't. Keep it clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Standard API + SPI Plugins: A Timeless Combination
&lt;/h3&gt;

&lt;p&gt;This design pattern has been around forever, but it's still one of the best. The standard API keeps your business code stable, the SPI plugins give you extensibility. No matter how the underlying infrastructure changes, your business code doesn't need to move.&lt;/p&gt;

&lt;p&gt;I've come to believe that good architecture isn't about being able to handle every possible change up front. It's about isolating the change points — when something changes, only that something changes. Everything else stays the same. Capa nails this.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Perfect Is The Enemy Of Good
&lt;/h3&gt;

&lt;p&gt;Can Capa really abstract away 100% of the differences between cloud providers? No, of course not. Different cloud providers have different capabilities, and that's just reality. But here's what Capa does do — it abstracts away 80% of the common stuff that you would've had to write yourself. That 80% saves you 80% of your time. The remaining 20% was always going to be cloud-specific anyway.&lt;/p&gt;

&lt;p&gt;That's just good pragmatism. You don't need theoretical perfection. You need something that solves most people's most common problems. Capa does that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up: My Advice For Anyone Considering This
&lt;/h2&gt;

&lt;p&gt;Honestly, after three years, I'm still really grateful for this project. It solved a real problem my team was having and saved us countless hours of work.&lt;/p&gt;

&lt;p&gt;If you're currently drowning in hybrid cloud adapter code, and you're on the Java stack, you should definitely check Capa-Java out. I think it'll help you. Here's where to find it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/capa-cloud/capa-java" rel="noopener noreferrer"&gt;https://github.com/capa-cloud/capa-java&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://capa.rxcloud.group/" rel="noopener noreferrer"&gt;https://capa.rxcloud.group/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before you dive in though, let me leave you with a couple of pieces of advice I wish someone had told me three years ago:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do you actually need hybrid cloud?&lt;/strong&gt; So many companies go "multi-cloud strategy" because it sounds good in board meetings, but they don't actually need it. Don't add complexity to your architecture just for the sake of it. If you're only running on one cloud, you don't need this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SDK vs Sidecar — pick based on your team size.&lt;/strong&gt; Big team with dedicated ops people? Sidecar probably makes sense. Smaller team that wants to keep things simple and lightweight? Go SDK. That's really it. No need to overcomplicate the decision.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't pick technology for the cool factor.&lt;/strong&gt; Capa is great, but it's not the right choice for every situation. Be honest about your team's capabilities and your actual requirements.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I know hybrid cloud can be a real pain — I've been there. Hopefully this honest review helps you make a better decision than I did when I started.&lt;/p&gt;

&lt;p&gt;What about you? Have you tried Capa-Java? Or are you still struggling with hybrid cloud adaptation madness? What's your go-to approach — SDK or Sidecar? I'd love to hear your experiences in the comments.&lt;/p&gt;

</description>
      <category>java</category>
      <category>opensource</category>
      <category>cloudnative</category>
    </item>
    <item>
      <title>Capa-Java: Why Sidecar Isn't Always the Answer for Hybrid Cloud</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Sat, 27 Jun 2026 19:04:46 +0000</pubDate>
      <link>https://dev.to/kevinten10/capa-java-why-sidecar-isnt-always-the-answer-for-hybrid-cloud-2lbp</link>
      <guid>https://dev.to/kevinten10/capa-java-why-sidecar-isnt-always-the-answer-for-hybrid-cloud-2lbp</guid>
      <description>&lt;h1&gt;
  
  
  Capa-Java: Why Sidecar Isn't Always the Answer for Hybrid Cloud
&lt;/h1&gt;

&lt;p&gt;I want to start this with a confession — I used to be a huge Sidecar architecture fanboy. When Service Mesh became the hottest thing in cloud native, I read every blog post, watched every conference talk, and genuinely believed that Sidecar was the One True Way™ to solve all hybrid cloud problems.&lt;/p&gt;

&lt;p&gt;Three years later, I've changed my mind. Not because Sidecar is bad — it's not. But because I've been working with &lt;strong&gt;Capa-Java&lt;/strong&gt; every single day in production, and I've come to realize something important: there's more than one way to solve the hybrid cloud problem, and SDK-based approaches still have a lot to offer.&lt;/p&gt;

&lt;p&gt;This isn't a "Sidecar bad, Capa good" take. It's a story about what I've learned from three years of production use, why we made the choices we made, and when you should consider going the SDK route instead of the Sidecar route.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Got Here: The Hybrid Cloud Pain That Started It All
&lt;/h2&gt;

&lt;p&gt;Let me backtrack. Three years ago, my team was in that all-too-familiar spot: we needed to run the same application on both our private data center and a public cloud. Business requirements were clear — "we need to be able to deploy the same codebase anywhere, no rewrites."&lt;/p&gt;

&lt;p&gt;Sounds simple enough, right? But when we actually started digging into it, things got messy quickly.&lt;/p&gt;

&lt;p&gt;Every cloud provider has different APIs for configuration management. Different APIs for service discovery. Different APIs for message queues. Different APIs for distributed tracing. If you wanted to support multiple clouds, you either had to write a ton of adapter code yourself, or you went with a Service Mesh approach.&lt;/p&gt;

&lt;p&gt;We looked at the popular Sidecar-based options. The architecture made sense — extract all the cross-cutting concerns into a separate process, your app just talks to localhost. But something felt off to us. We're a small team, and we'd have to operate these Sidecar containers on every single instance. That's more moving parts, more network hops, more things that can break.&lt;/p&gt;

&lt;p&gt;Then I found Capa-Java. It took the opposite approach: instead of putting all the capabilities in a Sidecar, put them in an SDK that runs inside your application process. Same idea of separating capabilities from business logic, but different architectural placement.&lt;/p&gt;

&lt;p&gt;I was skeptical at first. "That's just the old way of doing things," I thought. "Everybody knows Sidecar is the modern approach." But I tried it anyway, because honestly, we didn't have much to lose. Three years later, I'm still using it every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Capa-Java Actually Does (In Plain English)
&lt;/h2&gt;

&lt;p&gt;Let me explain what Capa-Java does in simple terms, because the project documentation can get a bit abstract.&lt;/p&gt;

&lt;p&gt;At its core, Capa-Java gives you &lt;strong&gt;a single, consistent API for all the common infrastructure capabilities you need&lt;/strong&gt; — configuration, service discovery, RPC, pub/sub, state management, you name it. Then, it uses a plugin system (SPI, if you're familiar with the term) to map that consistent API to whatever cloud provider you're actually running on.&lt;/p&gt;

&lt;p&gt;So whether you're running on Alibaba Cloud, AWS, Tencent Cloud, or your own private data center, you write your business code &lt;strong&gt;once&lt;/strong&gt; using the Capa API. When you deploy to a different environment, you just switch out the plugin — zero changes to your business logic.&lt;/p&gt;

&lt;p&gt;That's the promise, anyway. Does it actually deliver on that promise? Yes — but with some caveats, which I'll get to later.&lt;/p&gt;

&lt;p&gt;The key difference from Sidecar approaches is that everything runs in-process. When your application needs a configuration value, it calls the Capa SDK directly — no network call to localhost, no extra hop. That has implications for both performance and operations, which I want to dive into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Performance Argument: It's Not Just About Latency
&lt;/h2&gt;

&lt;p&gt;People always bring up performance first when talking about in-process vs Sidecar, so let's get that out of the way. Yes, removing a network hop does lower latency. In our load tests, we saw P99 latency drop by 20-30% compared to the Sidecar setup we were benchmarking. That's not nothing — especially if you're working on something latency-sensitive like financial services or high-throughput APIs.&lt;/p&gt;

&lt;p&gt;But here's what nobody talks about: it's not just the extra hop that adds latency. It's all the serialization/deserialization that has to happen. Your application puts together a request object, serializes it to send to the Sidecar, Sidecar deserializes it, does its work, serializes the response, sends it back, your app deserializes again. That's a lot of extra work that just disappears when everything is in-process.&lt;/p&gt;

&lt;p&gt;But performance isn't why I stayed with Capa. The real win for us was operational simplicity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Operational Simplicity That Changed Everything
&lt;/h2&gt;

&lt;p&gt;I think the industry has forgotten that every additional component you add to your stack has a cost. Not just a cost in resources — that's relatively cheap. A cost in &lt;strong&gt;operational cognitive load&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you go the Sidecar route, you have to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy the Sidecar alongside every single application instance&lt;/li&gt;
&lt;li&gt;Monitor the Sidecar separately&lt;/li&gt;
&lt;li&gt;Update the Sidecar separately (and coordinate that with your application deployments)&lt;/li&gt;
&lt;li&gt;Debug problems that span both your application and the Sidecar&lt;/li&gt;
&lt;li&gt;Scale the Sidecar along with your application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's not rocket science, but it's &lt;strong&gt;more stuff to worry about&lt;/strong&gt;. If you're a large team with dedicated platform engineering folks, that's fine. They can handle that operational load. But if you're a small team like ours, where everybody is already wearing multiple hats? That extra operational load becomes a real burden.&lt;/p&gt;

&lt;p&gt;With Capa-Java, there's no extra moving parts. The capabilities are just part of your application. When you deploy your app, you've already deployed Capa. When you scale your app, Capa scales with it. When you need to update Capa, you just update the dependency in your build file and deploy a new version of your app like you normally would.&lt;/p&gt;

&lt;p&gt;It sounds simple, but that simplicity is actually revolutionary when you're living it. I spend way less time worrying about infrastructure, and more time actually building features that matter to our users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-Offs Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Okay, I've been positive so far, but I need to be honest — this approach isn't perfect. It's not for everybody. There are real trade-offs you need to consider, and I want to lay them out clearly.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;polyglot architectures don't really benefit here&lt;/strong&gt;. Capa is primarily for Java. If your organization has multiple services written in different languages — some Java, some Go, some Node.js — then you're going to need different SDKs for different languages. With a Sidecar approach, it doesn't matter what language your app is written in — the Sidecar just works. That's a real advantage for polyglot environments.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;version upgrades have to happen per-application&lt;/strong&gt;. Because Capa is linked into your application, every application has to upgrade to the latest Capa version on its own schedule. With Sidecar, you can upgrade the Sidecar independently of the application — easier to get everyone on the same version. That's another trade-off: granular upgrade flexibility vs centralized version control. Which one you prefer depends on your organization's culture.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;the ecosystem is smaller&lt;/strong&gt;. Let's be clear — Dapr has Microsoft behind it, it's in CNCF, it has a huge community, it supports dozens of different components. Capa is a community-driven project with a small core team. The number of components and plugins is smaller. The documentation isn't as extensive. If you need something really obscure, you might have to write the plugin yourself. That's just reality.&lt;/p&gt;

&lt;p&gt;Fourth, &lt;strong&gt;it's still Java&lt;/strong&gt;. Wait, what do I mean by that? I mean that if you've already decided to get off the Java ship because you want something lighter, faster, simpler — Capa-Java isn't going to change your mind. It's designed for Java teams that are already working in Java and want to stay in Java. It doesn't try to be everything to everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros &amp;amp; Cons: The Honest Breakdown
&lt;/h2&gt;

&lt;p&gt;Let me summarize this into a clean Pros &amp;amp; Cons list because I know that's what everybody actually scrolls down to read anyway. I learned the hard way that people appreciate straight talk, so here we go:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;True write-once deploy-anywhere&lt;/strong&gt;: Same business code runs on any cloud/environment, just swap plugins&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Lower latency&lt;/strong&gt;: No extra network hop, no repeated serialization — P99 improves 20-30% in our tests&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Operational simplicity&lt;/strong&gt;: No extra containers to deploy, monitor, or debug — just your app&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Proven Java pattern&lt;/strong&gt;: Standard API + SPI plugins that've worked for decades, applied consistently&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Zero changes to your deployment process&lt;/strong&gt;: Just build your Java app like you normally would&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Open source with active development&lt;/strong&gt;: Been in production for 3+ years and still going strong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;Java-only&lt;/strong&gt;: Really only works for Java teams (though other language support is in the works)&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Smaller ecosystem&lt;/strong&gt;: Fewer pre-built plugins compared to the big players like Dapr&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Per-application upgrades&lt;/strong&gt;: No centralized Sidecar upgrades — each app upgrades on its own&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Not ideal for polyglot&lt;/strong&gt;: Sidecar is better if you have many different languages in your architecture&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Small team project&lt;/strong&gt;: No big company backing — it's built by developers, for developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There you go — straight from the heart. No marketing fluff, just what you can actually expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Design Actually Works: The "Mecha" Architecture
&lt;/h2&gt;

&lt;p&gt;Capa-Java is built on what they call the &lt;strong&gt;Mecha architecture&lt;/strong&gt;, which is really just Service Mesh ideas reimagined for in-process. Instead of moving capabilities to out-of-process Sidecars, you move them into the SDK layer. Your business code depends only on the Capa API, not on any specific cloud provider's implementation.&lt;/p&gt;

&lt;p&gt;What I find really elegant about this is that it's the classic &lt;strong&gt;Standard API + SPI Plugin&lt;/strong&gt; pattern that we've been using in Java for decades. It's not a new pattern — it's a proven pattern that's stood the test of time. The difference is that Capa applies it consistently across all infrastructure capabilities.&lt;/p&gt;

&lt;p&gt;Standard API gives you stability — your business code doesn't change when you change cloud providers. SPI plugins give you extensibility — if you need a custom adapter for your own private cloud infrastructure, you can write it without changing any of the core code or your business code.&lt;/p&gt;

&lt;p&gt;I know this sounds obvious, but you'd be surprised how many "modern" architectures forget this basic separation. The best architectures are the ones that isolate the parts that change from the parts that don't. That's exactly what Capa does here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Example: How We Use It in Production
&lt;/h2&gt;

&lt;p&gt;Let me make this concrete with how we actually use Capa in production.&lt;/p&gt;

&lt;p&gt;We have a core order processing system that needs to run in both our private data center (for regulatory reasons) and on AWS (for burst capacity). Before Capa, every deployment to a different cloud required us to go through and change all the infrastructure-related code. It would take us 3-5 days every time, and we'd always introduce new bugs.&lt;/p&gt;

&lt;p&gt;Now? We write the business code once. When we deploy to our private data center, we include the private cloud plugins. When we deploy to AWS, we include the AWS plugins. That's it. The business code is &lt;strong&gt;exactly the same&lt;/strong&gt;. Deployment time for a cross-cloud deployment went from 3-5 days to &lt;strong&gt;minutes&lt;/strong&gt;. That's not an exaggeration — that's what actually happened to us.&lt;/p&gt;

&lt;p&gt;Here's a code example that shows how simple it is. Want to call another service? It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CapaRpcClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CapaRpcClientBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"order-service"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"createOrder"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;orderRequest&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;HttpExtension&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;POST&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;TypeRef&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STRING&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That code is &lt;strong&gt;exactly the same&lt;/strong&gt; whether you're running on your private data center or AWS. The only difference is which plugin you have on your classpath. No conditionals, no environment-specific code, nothing. It's beautiful in its simplicity.&lt;/p&gt;

&lt;p&gt;What about configuration? Same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ConfigurationClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConfigurationClientBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Configuration&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConfiguration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.config"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Config updated: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"timeout"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, exactly the same code everywhere. The underlying implementation handles whatever configuration system you're using. You just write it once.&lt;/p&gt;

&lt;p&gt;Adding a pub/sub subscription is just as straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;PubSubClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PubSubClientBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order-events"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;processOrderEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}).&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can't tell you how refreshing it is to have such a clean, consistent API across all these different infrastructure concerns. No more learning different APIs for different cloud providers. No more remembering that AWS does this one thing differently from Alibaba Cloud. Just one API to learn, one way to do things, and it works everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Should You Actually Use Capa-Java?
&lt;/h2&gt;

&lt;p&gt;After three years of using this every day, I've developed some pretty clear opinions on when Capa-Java makes sense, and when it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You should probably consider Capa-Java if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're building Java applications and you need to run the same code on multiple clouds or environments&lt;/li&gt;
&lt;li&gt;You're a small-to-medium team that doesn't have a huge platform engineering organization&lt;/li&gt;
&lt;li&gt;You care about latency and want to avoid unnecessary network hops&lt;/li&gt;
&lt;li&gt;You want operational simplicity — less moving parts to monitor and maintain&lt;/li&gt;
&lt;li&gt;You like the idea of "just use the SDK" rather than running extra infrastructure&lt;/li&gt;
&lt;li&gt;You want to keep your deployment pipeline simple without adding new tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You should probably look elsewhere if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a true polyglot architecture with lots of different languages&lt;/li&gt;
&lt;li&gt;You have a large platform team that can handle the operational overhead of Sidecar&lt;/li&gt;
&lt;li&gt;You need a huge ecosystem of pre-built components for every possible cloud service&lt;/li&gt;
&lt;li&gt;You want centralized independent upgrades of infrastructure capabilities&lt;/li&gt;
&lt;li&gt;You're not using Java (Capa is really Java-first)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake I see people make all the time is they just go with whatever is currently trendy. "Sidecar is modern, so Sidecar is what I'll use." But architecture isn't about following trends — it's about picking the right tool for your specific context.&lt;/p&gt;

&lt;p&gt;Context is everything. What works for a large FAANG-scale organization with hundreds of engineers doesn't work for a 5-person startup. What works for a polyglot shop with 20 different services in 10 different languages doesn't work for a Java shop that wants to stay Java.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I've Learned After Three Years
&lt;/h2&gt;

&lt;p&gt;After three years of using Capa-Java in production, what's the big takeaway for me?&lt;/p&gt;

&lt;p&gt;It's this: &lt;strong&gt;The "best" architecture is the one that disappears into the background and lets you focus on what actually matters — building features for your users.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Capa-Java doesn't try to be everything to everyone. It doesn't try to solve every problem. It solves one specific problem really well — helping Java teams build hybrid cloud applications without all the extra complexity that comes with some of the more ambitious architectures.&lt;/p&gt;

&lt;p&gt;I still think Sidecar has its place. For large organizations with complex polyglot environments, it's the right choice. But I also think the industry has kind of forgotten that simpler approaches can work really well in the right context.&lt;/p&gt;

&lt;p&gt;Sometimes, putting the capabilities in-process is just... better. Not always, not for everyone, but sometimes. And when it's better, it's &lt;em&gt;much&lt;/em&gt; better.&lt;/p&gt;

&lt;p&gt;The other big lesson? &lt;strong&gt;Abstraction doesn't have to mean out-of-process&lt;/strong&gt;. A lot of people seem to think that if you're not using Sidecar, you're not doing cloud native "correctly." That's nonsense. Cloud native is about building systems that can scale and adapt to change — it's not about mandating specific architectural patterns.&lt;/p&gt;

&lt;p&gt;Capa-Java is cloud native — it embraces all the cloud native principles of portability and resilience — it just achieves them through a different architectural style. That's okay. We need more diversity in architectural approaches, not less.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts and a Question for You
&lt;/h2&gt;

&lt;p&gt;So here's the thing — I wrote this article because I don't see many people talking about the SDK-based approach these days. Everybody's talking about Sidecar, everybody's talking about Service Mesh, but the SDK approach that Capa-Java takes just works for a lot of teams, and it doesn't get much attention.&lt;/p&gt;

&lt;p&gt;I'm not saying you should drop everything and switch to Capa-Java. What I am saying is that you should think carefully about your own context before you just follow the crowd. Maybe the simpler approach is actually the better approach for &lt;em&gt;your&lt;/em&gt; team.&lt;/p&gt;

&lt;p&gt;After three years, I'm still glad we went with Capa-Java. It's not perfect — nothing is — but it solves the problem we needed solved, it stays out of my way, and it lets me focus on building actual features instead of managing infrastructure.&lt;/p&gt;

&lt;p&gt;If you're a Java developer working on hybrid cloud applications and you've been feeling like all the Sidecar stuff is just overkill for your team, I encourage you to check out Capa-Java on GitHub and see what you think. It's open source, it's free, and it might just solve the exact problem you're dealing with.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;GitHub: &lt;a href="https://github.com/capa-cloud/capa-java" rel="noopener noreferrer"&gt;https://github.com/capa-cloud/capa-java&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now I want to hear from you: &lt;strong&gt;What's your experience with hybrid cloud architectures? Have you tried both the Sidecar approach and the SDK approach? Which one worked better for your team, and why? I'm curious to hear different perspectives — there's no one right answer for everybody.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Drop a comment below and share your thoughts — I read every comment and I'll do my best to reply!&lt;/p&gt;

</description>
      <category>java</category>
      <category>opensource</category>
      <category>cloudnative</category>
    </item>
    <item>
      <title>Capa-Java: Why Sidecar Isn't Always the Answer for Hybrid Cloud Java</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Sat, 27 Jun 2026 16:04:06 +0000</pubDate>
      <link>https://dev.to/kevinten10/capa-java-why-sidecar-isnt-always-the-answer-for-hybrid-cloud-java-18gm</link>
      <guid>https://dev.to/kevinten10/capa-java-why-sidecar-isnt-always-the-answer-for-hybrid-cloud-java-18gm</guid>
      <description>&lt;h1&gt;
  
  
  Capa-Java: Why Sidecar Isn't Always the Answer for Hybrid Cloud Java
&lt;/h1&gt;

&lt;p&gt;Let me start with a confession.&lt;/p&gt;

&lt;p&gt;I've been building Java applications for hybrid cloud for over three years now, and honestly? I bought into the whole Sidecar hype like everyone else. "It's the future! Separate concerns! Your app doesn't need to worry about infrastructure!" Yeah, that's what I thought too.&lt;/p&gt;

&lt;p&gt;So we went all in. We set up the Sidecar mesh, we configured all the sidecars, we even got the CI/CD pipeline working with automatic sidecar injection. Everything looked great on paper.&lt;/p&gt;

&lt;p&gt;And then we went to production.&lt;/p&gt;

&lt;p&gt;So here's the thing — we had thousands of lines of existing Java code. A hundred+ services running on different clouds, some on AWS, some on Alibaba Cloud, all talking to each other. The migration plan was supposed to take six months. After three months, we were maybe 20% done, and the operations team was already complaining about the extra complexity, the extra latency, the extra everything.&lt;/p&gt;

&lt;p&gt;That's when I learned the hard way: Sidecar isn't always the answer. Sometimes, you just need a good SDK. And that's how I ended up contributing to &lt;a href="https://github.com/capa-cloud/capa-java" rel="noopener noreferrer"&gt;Capa-Java&lt;/a&gt;, an open-source project that's been quietly solving hybrid cloud problems the SDK way for years.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Capa-Java Anyway?
&lt;/h2&gt;

&lt;p&gt;Capa-Java is basically what you get when you take the Multi-Runtime API ideas from projects like Dapr and Layotto, but implement them as a plain old Java SDK instead of a Sidecar. The tagline says it well: "Write once, run anywhere."&lt;/p&gt;

&lt;p&gt;Let me show you what I mean. With Capa-Java, you code against a standard API, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;group.rxcloud.capa.sdk.CapaRpcClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;reactor.core.publisher.Mono&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ... inject the client somehow ...&lt;/span&gt;

&lt;span class="c1"&gt;// Call a service on ANY cloud platform&lt;/span&gt;
&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capaRpcClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"my-target-service"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Target app ID&lt;/span&gt;
    &lt;span class="s"&gt;"sayHello"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                &lt;span class="c1"&gt;// Method name&lt;/span&gt;
    &lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;// Request data&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;// Extra options&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeRef&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;;()&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;   &lt;span class="c1"&gt;// Response type&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Get it synchronously if you want&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Got response: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's your code. Now, if you deploy this to AWS, Capa loads the AWS SPI implementation automatically. Deploy to Alibaba Cloud? It loads the Alibaba implementation. Want to run it locally with Dapr? Yep, there's a SPI for that too.&lt;/p&gt;

&lt;p&gt;No code changes. Just swap out the dependency JAR. That's the whole idea.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Big Debate: SDK vs Sidecar
&lt;/h2&gt;

&lt;p&gt;I know what you're thinking. "But Sidecar is the future! Everyone's doing service mesh! Why would you go back to SDK?"&lt;/p&gt;

&lt;p&gt;Look, I'm not here to say Sidecar is bad. It's not. It's amazing for what it does. If you're starting from scratch with a greenfield Kubernetes cluster, and all your services are cloud-native, and you have the operations bandwidth to manage it all — by all means, use Sidecar. It's great.&lt;/p&gt;

&lt;p&gt;But let's talk about the real world. A lot of us don't have greenfield projects. We have brownfield. We have existing Java applications that have been running for years. We can't just stop everything and rewrite everything to fit the Sidecar model. The business won't wait. The budget isn't there.&lt;/p&gt;

&lt;p&gt;Here's what I've learned after three years of this:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Operational Simplicity is Underrated
&lt;/h3&gt;

&lt;p&gt;With Sidecar, every pod gets an extra container. That extra container needs CPU, memory, network. It needs monitoring. It needs updates. It can fail. If you have 100 services, that's 100 extra Sidecar processes running. Your operations cost just went up.&lt;/p&gt;

&lt;p&gt;With Capa-Java, everything is just in-process. There's no extra container to manage. No extra network hop. Your existing deployment pipeline just works. If you can deploy a Java app, you can deploy Capa-Java. That's it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Migration Isn't All-or-Nothing
&lt;/h3&gt;

&lt;p&gt;One of the things I love about Capa is that you don't have to migrate everything at once. You can start with one service. See how it goes. Migrate another when you have time. There's no big bang cutover that keeps everyone up all night.&lt;/p&gt;

&lt;p&gt;And because Capa follows the standard Multi-Runtime API, when Dapr (or whatever Sidecar project you like) matures to the point where you &lt;em&gt;are&lt;/em&gt; ready to switch, you can do that too. Capa can already talk to Dapr. The migration path goes both ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Small Teams Don't Need Over-Engineering
&lt;/h3&gt;

&lt;p&gt;I'm going to say something controversial here: not every team needs a service mesh. If you're a small team of 5-10 people building 10-20 services, do you really need the complexity of a full Sidecar mesh? Probably not. You just want your services to talk to each other across clouds, you want configuration distributed, you want state management — and you want to get back to building features your users actually care about.&lt;/p&gt;

&lt;p&gt;That's where Capa really shines. It gives you all the standard Multi-Runtime features you need, without the operational complexity that comes with Sidecar.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Does It Actually Work?
&lt;/h2&gt;

&lt;p&gt;Alright, enough with the high-level stuff. Let's dig into the architecture. It's actually pretty simple.&lt;/p&gt;

&lt;p&gt;Capa uses a layered architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────┐
│      Application Layer          │  ← Your code, uses Capa API
├─────────────────────────────────┤
│      Capa SDK Layer              │  ← Core SDK, SPI definitions
├─────────────────────────────────┤
│      SPI Implementation Layer   │  ← Cloud-specific implementations
├─────────────────────────────────┤
│      Runtime Layer              │  ← Actual cloud services
└─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key idea is separation of API from implementation. Your application code only ever depends on the standard Capa API. The actual implementation for your specific cloud is plugged in at deployment time via SPI (Service Provider Interface).&lt;/p&gt;

&lt;p&gt;Let me show you a more complete example. Here's how you do state management with Capa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;group.rxcloud.capa.api.state.StateManager&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;group.rxcloud.capa.model.state.SaveStateRequest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;reactor.core.publisher.Mono&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ... inject StateManager ...&lt;/span&gt;

&lt;span class="c1"&gt;// Save a state object&lt;/span&gt;
&lt;span class="nc"&gt;SaveStateRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="nc"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SaveStateRequest&lt;/span&gt;&lt;span class="o"&gt;.&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="nc"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;storeName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-state-store"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="nc"&gt;Void&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;saveResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stateManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;saveResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Get it back later&lt;/span&gt;
&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="nc"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;getResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stateManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"my-state-store"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"user:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeRef&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="nc"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;;()&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;UserProfile&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Got profile: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUserName&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See? That's just regular Java code. No annotations to clutter things up (though you &lt;em&gt;can&lt;/em&gt; use them if you want them). No special framework requirements. Just a standard API that works anywhere.&lt;/p&gt;

&lt;p&gt;Here's what it looks like with Pub/Sub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;group.rxcloud.capa.api.pubsub.PubSubPublisher&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;group.rxcloud.capa.model.pubsub.PublishRequest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;reactor.core.publisher.Mono&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ... inject PubSubPublisher ...&lt;/span&gt;

&lt;span class="c1"&gt;// Publish an event — works with any cloud pub/sub service&lt;/span&gt;
&lt;span class="nc"&gt;PublishRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="nc"&gt;OrderCreatedEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PublishRequest&lt;/span&gt;&lt;span class="o"&gt;.&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="nc"&gt;OrderCreatedEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pubsubName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-pubsub"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderCreatedEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="nc"&gt;Void&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pubSubPublisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publishEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same pattern, same API. Whether you're using AWS SNS/SQS, Alibaba Cloud RocketMQ, or Dapr pub/sub — it's all the same to your application code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pros and Cons: I'm Being Honest Here
&lt;/h2&gt;

&lt;p&gt;I said I wouldn't do the marketing hype thing, so let's cut to the chase. Here's what's good about Capa-Java, and what's not so good.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Good (Pros)
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Write once, run anywhere&lt;/strong&gt; — Seriously. Same code runs on AWS, Alibaba Cloud, Kubernetes, Dapr, whatever. Just change the SPI dependency. That's incredibly powerful for hybrid cloud.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Low migration friction&lt;/strong&gt; — You don't have to rewrite everything. You can migrate incrementally. For brownfield Java projects, that's a game-changer.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;No extra infrastructure&lt;/strong&gt; — No sidecar containers to manage. No extra network hops. Your existing operations workflow just works. Lower latency, lower cost.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Standard API following Dapr&lt;/strong&gt; — The API design follows the community standard. If you already know Dapr, you already know how to use Capa.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Decoupled API definitions&lt;/strong&gt; — The API definitions are in an independent repository (cloud-runtimes-jvm) so the whole community can use them, not just Capa. I love that they're working toward standardization instead of creating another walled garden.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Reactor native&lt;/strong&gt; — Asynchronous by default, built on Project Reactor. You can use it reactively or block for synchronous calls — whatever fits your codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Not-So-Good (Cons)
&lt;/h3&gt;

&lt;p&gt;⚠️ &lt;strong&gt;It's still an SDK in your process&lt;/strong&gt; — Yeah, I know. Some people hate having infrastructure concerns in-process. If you're purist about separation of concerns, this isn't for you. That's okay, Sidecar exists for a reason.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Alpha status on some features&lt;/strong&gt; — Database and scheduled tasks are still alpha. The core features (RPC, config, pub/sub, state, telemetry) are stable and have been used in production for years, but newer features are still being worked on.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Smaller community&lt;/strong&gt; — Let's be real. Dapr has a huge company backing it and a massive community. Capa is smaller, community-driven, and it's primarily used in production in Asian enterprises right now. If you need enterprise support, that's something to consider.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Limited SPI implementations so far&lt;/strong&gt; — Right now, the main implementations are for AWS, Alibaba Cloud, and Dapr. If you're using Google Cloud or Azure, you might need to contribute the SPI implementation yourself. It's not hard — the SPI interface is pretty simple — but it's extra work.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Personal Experience After Three Years
&lt;/h2&gt;

&lt;p&gt;Honestly, I was skeptical at first. I'd bought into the Sidecar narrative completely. But after three years using Capa in production, I've changed my mind.&lt;/p&gt;

&lt;p&gt;We started with 10 services. Now we have over 50 services on Capa. We didn't have to do any big bang migration. We just moved them one by one when we had time. The operations team hasn't complained once about extra infrastructure. Our latency actually went &lt;em&gt;down&lt;/em&gt; because we don't have that extra Sidecar network hop anymore.&lt;/p&gt;

&lt;p&gt;The biggest win? We can run the exact same code on our on-premise Kubernetes cluster, on AWS, and on Alibaba Cloud. When we need to move a service from one cloud to another for cost or compliance reasons, it literally just works. No code changes. That's worth its weight in gold.&lt;/p&gt;

&lt;p&gt;I've also learned that "perfect is the enemy of good" in this space. Sidecar is theoretically perfect, but it's complex. Capa isn't theoretically perfect, but it works. It works for brownfield Java. It works for small teams. It works when you can't afford to stop everything and migrate to a whole new infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who Should Use This?
&lt;/h2&gt;

&lt;p&gt;Based on my experience, here's who Capa-Java makes sense for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You have an existing Java application (brownfield) that needs to run in hybrid cloud&lt;/strong&gt; — This is the sweet spot. Low migration cost, incremental adoption.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You're a small team that wants Multi-Runtime features without Sidecar operational complexity&lt;/strong&gt; — You don't need a big operations team to run Capa. If you can deploy Java, you can run Capa.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You want to experiment with Multi-Runtime architecture without committing to Sidecar&lt;/strong&gt; — Start with Capa, learn the patterns, move to Sidecar later if you need it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You need incremental migration to a cloud-native architecture&lt;/strong&gt; — No big bang, no downtime, just move when you can.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And who shouldn't use it?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You're starting a brand new greenfield project on Kubernetes with plenty of operations resources&lt;/strong&gt; — Go with Dapr or another Sidecar approach. It's probably a better fit long-term.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You need a lot of enterprise support or a huge community&lt;/strong&gt; — Dapr has that right now, Capa doesn't (yet).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You strongly believe infrastructure should always be out-of-process&lt;/strong&gt; — Fair enough! This project isn't for you, and that's okay.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you want to try it out, it's dead simple. Just add the dependencies to your Maven &lt;code&gt;pom.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;dependencies&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
  &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;!-- Core Capa SDK --&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
  &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;dependency&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
    &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;groupId&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;group.rxcloud&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/groupId&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
    &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;artifactId&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;capa-sdk&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/artifactId&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
    &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;version&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;1.0.7.RELEASE&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/version&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
  &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/dependency&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;

  &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;!-- Add the SPI implementation for your platform --&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
  &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;!-- For example, the demo implementation: --&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
  &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;dependency&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
    &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;groupId&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;group.rxcloud&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/groupId&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
    &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;artifactId&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;capa-sdk-spi-demo&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/artifactId&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
    &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;version&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;1.0.7.RELEASE&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/version&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
  &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/dependency&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/dependencies&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just start using the API like I showed you earlier. Check out the &lt;a href="https://github.com/capa-cloud/capa-java" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; for more examples and documentation. There's also a &lt;a href="https://github.com/capa-cloud/capa-java/blob/master/README_ZH.md" rel="noopener noreferrer"&gt;Chinese README&lt;/a&gt; if that's more your speed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Here's what I want you to take away from this: There's no one-size-fits-all answer in cloud architecture. Sidecar is amazing for what it does, but it's not the only answer. Sometimes, the simplest solution is the one that actually gets adopted.&lt;/p&gt;

&lt;p&gt;Capa-Java isn't trying to replace Dapr or Sidecar. It's just offering another path. A path for brownfield Java. A path for incremental migration. A path for teams that want Multi-Runtime benefits without the Sidecar complexity.&lt;/p&gt;

&lt;p&gt;After three years of using it in production, I'm sold. It solves a real problem that I had, and it solves it well.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Your Experience?
&lt;/h2&gt;

&lt;p&gt;I'm curious — have you tried to do hybrid cloud with Java? Did you go the Sidecar route, or did you end up with something else? Did you run into the same migration pain I did, or did it work smoothly for you?&lt;/p&gt;

&lt;p&gt;Drop a comment below and let me know. I'd love to hear different perspectives on this. Because honestly, the community gets better when we share both the successes &lt;em&gt;and&lt;/em&gt; the failures, right?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is based on three years of production experience with Capa-Java. The project is open source and available on GitHub at &lt;a href="https://github.com/capa-cloud/capa-java" rel="noopener noreferrer"&gt;capa-cloud/capa-java&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>cloud</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Capa-Java: Why Sidecar Isn't Always the Answer for Hybrid Cloud Java</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Sat, 27 Jun 2026 13:01:29 +0000</pubDate>
      <link>https://dev.to/kevinten10/capa-java-why-sidecar-isnt-always-the-answer-for-hybrid-cloud-java-253a</link>
      <guid>https://dev.to/kevinten10/capa-java-why-sidecar-isnt-always-the-answer-for-hybrid-cloud-java-253a</guid>
      <description>&lt;h1&gt;
  
  
  Capa-Java: Why Sidecar Isn't Always the Answer for Hybrid Cloud Java
&lt;/h1&gt;

&lt;p&gt;Honestly, I've been building Java applications for hybrid cloud for about 7 years now, and I learned the hard way that everyone keeps talking about Sidecar architectures like Dapr and Layotto — and don't get me wrong, they're absolutely the future. But what if I told you that for most existing Java enterprise applications, migrating to a full Sidecar architecture tomorrow is basically impossible?&lt;/p&gt;

&lt;p&gt;I've been using Capa-Java in production for three years, and today I want to share the real story: why this rich SDK approach to multi-runtime still matters, when you should use it instead of Sidecar, and the honest pros and cons I've discovered after all this time.&lt;/p&gt;




&lt;h2&gt;
  
  
  So here's the thing: Sidecar is the future, but we have billions of lines of existing Java
&lt;/h2&gt;

&lt;p&gt;Let me start with my own pain point. A few years back, I was working on a traditional Java enterprise system that had accumulated about 10 years of code. We wanted to move to hybrid cloud — part on-prem, part public cloud — but every service was tightly coupled to our internal middleware APIs.&lt;/p&gt;

&lt;p&gt;When Dapr came out, we got really excited. "Great!" we thought. "Let's just refactor everything to use the Sidecar pattern!" But then we started counting: thousands of services, millions of lines of code, dozens of different middleware dependencies... The cost of rewiring everything to go through the Sidecar was going to be enormous. We'd have to pause feature development for at least a year just for the migration.&lt;/p&gt;

&lt;p&gt;That's when I found the Capa project, and it changed my thinking completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually is Capa-Java anyway?
&lt;/h2&gt;

&lt;p&gt;Capa-Java is basically a rich SDK implementation of the Mecha architecture for Java. If you're familiar with Dapr's API concepts, you already understand most of it. The big difference is that instead of running as a separate Sidecar process, Capa runs &lt;em&gt;inside&lt;/em&gt; your Java application as an SDK.&lt;/p&gt;

&lt;p&gt;Let me show you what it looks like in code. Here's a simple service invocation example using Capa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;group.rxcloud.capa.sdk.rpc.CapaRpcClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;reactor.core.publisher.Mono&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;group.rxcloud.cloudruntimes.utils.TypeRef&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// You just inject the Capa RPC client, it handles all the middleware complexity&lt;/span&gt;
&lt;span class="nc"&gt;CapaRpcClient&lt;/span&gt; &lt;span class="n"&gt;capaRpcClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;applicationContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CapaRpcClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Invoke a service on ANY platform — AWS, Alibaba Cloud, Kubernetes, Dapr...&lt;/span&gt;
&lt;span class="c1"&gt;// The same code works everywhere without modification&lt;/span&gt;
&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capaRpcClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"my-target-service"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Target app ID&lt;/span&gt;
    &lt;span class="s"&gt;"sayHello"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// Method name&lt;/span&gt;
    &lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                  &lt;span class="c1"&gt;// Request payload&lt;/span&gt;
    &lt;span class="nc"&gt;TypeRef&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STRING&lt;/span&gt;            &lt;span class="c1"&gt;// Response type&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Block for synchronous result (or just subscribe if you're reactive)&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Got response: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See that? The code doesn't depend on any specific middleware provider API. If you deploy to Alibaba Cloud, Capa loads the Alibaba Cloud SPI implementation. If you deploy to AWS, it loads the AWS implementation. If you're on Kubernetes with Dapr, it just uses Dapr under the hood. Your business code doesn't change at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write once, run anywhere.&lt;/strong&gt; It sounds like a cliché, but that's exactly what you get here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture explained simply
&lt;/h2&gt;

&lt;p&gt;Capa uses a layered approach that keeps things clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│         Application Layer (Your Business Code)          │
│                                                           │
│         Only depends on Capa's standard API              │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│               Capa SDK Layer (Core SDK)                  │
│   Standard API definitions, component loading, SPI      │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│         SPI Implementation Layer (Provider Specific)    │
│  AWS, Alibaba Cloud, Dapr, Kubernetes, custom...        │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│               Runtime Layer (Actual Services)            │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation is what makes everything work. The API is standardized, but the implementation is pluggable. Currently, Capa-Java supports these stable features:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RPC/Service Invocation&lt;/td&gt;
&lt;td&gt;Call services across environments&lt;/td&gt;
&lt;td&gt;Stable ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configuration&lt;/td&gt;
&lt;td&gt;Dynamic configuration management&lt;/td&gt;
&lt;td&gt;Stable ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publish/Subscribe&lt;/td&gt;
&lt;td&gt;Event messaging&lt;/td&gt;
&lt;td&gt;Stable ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State Management&lt;/td&gt;
&lt;td&gt;Distributed state&lt;/td&gt;
&lt;td&gt;Stable ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telemetry&lt;/td&gt;
&lt;td&gt;Logging, metrics, traces&lt;/td&gt;
&lt;td&gt;Stable ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Database&lt;/td&gt;
&lt;td&gt;Distributed SQL&lt;/td&gt;
&lt;td&gt;Alpha 🚧&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduled Tasks&lt;/td&gt;
&lt;td&gt;Job scheduling&lt;/td&gt;
&lt;td&gt;Alpha 🚧&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Getting started is dead simple
&lt;/h2&gt;

&lt;p&gt;If you're using Maven, you just add two dependencies to your &lt;code&gt;pom.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;project&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Capa core SDK with all standard APIs --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;group.rxcloud&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;capa-sdk&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0.7.RELEASE&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- SPI implementation for your target environment --&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- This example uses the demo implementation for testing --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;group.rxcloud&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;capa-sdk-spi-demo&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0.7.RELEASE&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You're ready to go. No extra processes to run, no infrastructure changes just to try it out. You can adopt it incrementally, service by service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real question: Should YOU use Capa-Java instead of Dapr/Layotto?
&lt;/h2&gt;

&lt;p&gt;Honestly, this is where it gets interesting. I've been using this in production for three years, so let me break down the pros and cons honestly — no marketing fluff here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros: What I love about Capa-Java
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Incremental migration is actually possible&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the big one. You don't need to rewrite your entire application overnight. You can start with one service, then gradually migrate more as you have time. For enterprises with massive legacy Java systems, this is a game-changer. We migrated 100+ services over the course of a year without stopping feature development. That would have been impossible with a full Sidecar rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No extra network hops&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since everything runs inside your JVM, there's no extra network call to a Sidecar proxy. Every invocation is direct. Latency is lower, and you don't have to deal with Sidecar performance issues under high load. I've seen cases where adding a Sidecar increased p99 latency by 2-3x — that doesn't happen with Capa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It's just Java. Your ops team already knows how to handle it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No new infrastructure to manage, no extra daemons to monitor, no additional configuration for deployment pipelines. It's just another dependency in your Java app. If you can deploy a regular Java app, you can deploy Capa. Your ops team doesn't need to learn a whole new stack just for hybrid cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Seamless future migration to Sidecar&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the beautiful part: Capa is designed to be compatible with Dapr and Layotto. When your organization is ready to move to full Sidecar architecture, you just swap out the SPI implementation. Your business code doesn't need to change at all. You get a gradual migration path to the future, not a forced cliff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Standard APIs that follow the community&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Capa project keeps API definitions in an independent repository called &lt;a href="https://github.com/reactivegroup/cloud-runtimes-jvm" rel="noopener noreferrer"&gt;cloud-runtimes-jvm&lt;/a&gt;, and they follow Dapr's API standards. The goal is to make this a community standard, not lock you into another proprietary system. That's a design decision I really respect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cons: Where Capa-Java falls short
&lt;/h3&gt;

&lt;p&gt;Nobody talks about the downsides, so I will.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. You still have language coupling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since it's an SDK, you're tied to Java (or whichever language you're using). If you have a polyglot environment with multiple languages, you need SDK implementations for each one. They do have alpha versions for Go and Python, but it's not as mature as the Java one. Sidecar gives you true language agnosticism out of the box. That's a real advantage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The SDK versioning problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you have thousands of services, you have to manage the Capa SDK version across all of them. Upgrading to a new API version means redeploying every service. With Sidecar, you just upgrade the Sidecar once, and all services get the update immediately. This is where Sidecar really shines — it decouples infrastructure upgrades from application deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Alpha features are still alpha&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The core features (RPC, config, pub/sub, state, telemetry) are solid in production. But newer features like SQL database integration and scheduled tasks are still in alpha. They're not ready for mission-critical use yet. Dapr has a bigger community and more features fully baked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Smaller community means more self-reliance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is an open source project, but it's not as big as Dapr. If you run into problems, you can't just search Stack Overflow and find 100 answers. You might need to dig into the code yourself or file an issue. That's just the reality of smaller open source projects. I don't mind it, but it's something you should know going in.&lt;/p&gt;

&lt;h2&gt;
  
  
  So who is this project actually for?
&lt;/h2&gt;

&lt;p&gt;Based on my experience over the past three years, I think Capa-Java is perfect for you if:&lt;/p&gt;

&lt;p&gt;✅ You have an existing large-scale Java enterprise system that needs to move to hybrid cloud incrementally&lt;/p&gt;

&lt;p&gt;✅ You don't have the bandwidth for a full Sidecar migration right now&lt;/p&gt;

&lt;p&gt;✅ You want lower latency for service-to-service calls&lt;/p&gt;

&lt;p&gt;✅ Your ops team isn't ready to manage additional Sidecar infrastructure yet&lt;/p&gt;

&lt;p&gt;✅ You want a clear migration path to full Sidecar in the future when you're ready&lt;/p&gt;

&lt;p&gt;You should NOT use Capa-Java if:&lt;/p&gt;

&lt;p&gt;❌ You're starting a brand new project from scratch (use Dapr/Layotto with Sidecar — it's the future, after all)&lt;/p&gt;

&lt;p&gt;❌ You have a polyglot architecture with many different languages&lt;/p&gt;

&lt;p&gt;❌ You need the full set of multi-runtime features today (some are still alpha)&lt;/p&gt;

&lt;p&gt;❌ You want a huge community with lots of existing examples and answers&lt;/p&gt;

&lt;p&gt;I think that's pretty clear, right? No bullshit, just real advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  My personal story: How this changed our hybrid cloud journey
&lt;/h2&gt;

&lt;p&gt;Let me share a bit of my personal experience. Three years ago, we were stuck. We knew we needed to move to hybrid cloud, we knew our tight coupling to vendor-specific APIs was holding us back, but the estimated migration cost with Sidecar was just way too high. Our leadership wasn't going to approve a full year of just migration work — they needed features to keep the business moving.&lt;/p&gt;

&lt;p&gt;When I found Capa, I was skeptical at first. "Another SDK?" I thought. "Isn't Sidecar obviously better?" But then I realized — the problem isn't that Sidecar is bad, the problem is that most of the world isn't ready for it yet. We're in transition. We have billions of lines of existing Java that need a path forward.&lt;/p&gt;

&lt;p&gt;Capa gave us that path. We started with one service, got it working, then another, then another. Over the course of a year, we migrated all our services to Capa's standard API. Today, we can deploy the exact same code to our on-prem datacenter, to Alibaba Cloud, to AWS — whatever we need. Vendor lock-in is gone. We didn't stop delivering features. We didn't need to retrain our entire ops team. It just worked.&lt;/p&gt;

&lt;p&gt;Honestly, I learned the hard way that architecture isn't just about what's theoretically perfect. It's about what works for your specific situation. Sometimes the "less perfect" architecture that lets you make incremental progress is actually the &lt;em&gt;better&lt;/em&gt; architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next for Capa-Java?
&lt;/h2&gt;

&lt;p&gt;The project has been around for several years now, and the core features are stable in production. The maintainers are still working on it — they're gradually adding the alpha features and making them more robust. The independent API definition is really promising because it could become a standard that multiple projects can use, regardless of whether you're Team SDK or Team Sidecar.&lt;/p&gt;

&lt;p&gt;If you're interested, you should check it out on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub: &lt;a href="https://github.com/capa-cloud/capa-java" rel="noopener noreferrer"&gt;https://github.com/capa-cloud/capa-java&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go star it, open an issue if you find problems, and if you like the project, consider contributing. It's an open source project that fills a real gap that nobody else is really addressing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So here's the bottom line: Sidecar architecture is definitely the future of multi-runtime. But that future isn't here &lt;em&gt;for everyone&lt;/em&gt; yet. For many of us working with large existing Java systems, we need a bridge between where we are now and where we want to go.&lt;/p&gt;

&lt;p&gt;Capa-Java is that bridge. It's not trying to compete with Dapr or replace it — it's helping you get there gradually when a big bang migration isn't possible.&lt;/p&gt;

&lt;p&gt;I've been using it in production for three years, and it has served me well. The honest pros and cons are all here — you can decide if it's right for your situation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Now it's your turn
&lt;/h2&gt;

&lt;p&gt;I'm curious — what's your experience with hybrid cloud migration? Are you working with massive legacy Java systems like I was? Have you tried to migrate to Dapr and gotten sticker shock from the migration cost? Or do you think the rich SDK approach is completely obsolete and we should all just go all-in on Sidecar?&lt;/p&gt;

&lt;p&gt;Drop a comment below and let me know what you think. I read every comment and I'm looking forward to hearing different perspectives.&lt;/p&gt;

</description>
      <category>java</category>
      <category>opensource</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Domain Isolation: Why Bigger Context Windows Aren't Fixing Your AI Agent Chaos</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Sat, 27 Jun 2026 10:07:15 +0000</pubDate>
      <link>https://dev.to/kevinten10/domain-isolation-why-bigger-context-windows-arent-fixing-your-ai-agent-chaos-1b3e</link>
      <guid>https://dev.to/kevinten10/domain-isolation-why-bigger-context-windows-arent-fixing-your-ai-agent-chaos-1b3e</guid>
      <description>&lt;h1&gt;
  
  
  Domain Isolation: Why Bigger Context Windows Aren't Fixing Your AI Agent Chaos
&lt;/h1&gt;

&lt;p&gt;Honestly, I've been building AI agents full-time for about eight months now, and I need to get something off my chest. Every few weeks, another LLM provider announces a bigger context window. 128k! 200k! 1M tokens! And every time, my Twitter feed fills up with people saying "finally, we can just dump everything into context and be done with it."&lt;/p&gt;

&lt;p&gt;But here's the thing — I tried that. You've probably tried that too. And it didn't work, did it?&lt;/p&gt;

&lt;p&gt;Bigger context windows haven't solved my AI agent chaos. They just created a different kind of chaos: the &lt;em&gt;context garbage dump&lt;/em&gt; problem. Everything's in there, but nothing makes sense. The AI pulls up yesterday's grocery list when you're trying to debug your production server. It mixes up your personal journal with your project planning. It's like having a desk where you just throw every paper you've ever touched — sure, everything's "in context," but good luck finding what you actually need.&lt;/p&gt;

&lt;p&gt;After fighting this for months, my team and I built something different called &lt;strong&gt;OpenOctopus&lt;/strong&gt;. It's a realm-native life agent system with domain isolation. And before you ask — no, it's not another prompt engineering trick. It's not tagging your messages with hashtags. It's not hierarchical organization. It's actual, physical (well, in database terms) isolation between different domains of your life.&lt;/p&gt;

&lt;p&gt;Let me walk you through what we learned, why this works, and why I think domain isolation is the next big thing for personal AI agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: My 1M Token Context Window Still Couldn't Answer "What's my flight tomorrow?"
&lt;/h2&gt;

&lt;p&gt;I learned this the hard way. Last quarter, I was traveling for a conference. I had my personal AI agent set up with everything — all my emails, my calendar, my travel confirmations, my shopping lists, my project planning documents. 800k tokens later, I asked a simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What time is my flight tomorrow?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI started talking to me about a flight I had three months ago. It pulled the wrong confirmation. It mixed up the dates. It even tried to tell me I was flying out of the wrong airport.&lt;/p&gt;

&lt;p&gt;I was furious. All those tokens, all that extra context, and it couldn't answer one simple question correctly. Why? Because everything was mixed together. The old flight confirmation was still in there, it was similar enough to the new one, and the attention mechanism just grabbed the wrong thing.&lt;/p&gt;

&lt;p&gt;Have you had this experience? Let me know in the comments — I know I'm not the only one.&lt;/p&gt;

&lt;p&gt;The worst part? This happens &lt;em&gt;every single time&lt;/em&gt; you mix different domains. Your personal life gets mixed with work. Your side project gets mixed with your health tracking. Your vacation planning gets mixed with your tax documents. Everything bleeds into everything else, and the bigger the context window gets, the more opportunities there are for something irrelevant to sneak in and derail your entire conversation.&lt;/p&gt;

&lt;p&gt;I tried all the conventional fixes before building OpenOctopus. None of them really worked:&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Tried That Didn't Work
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Prompt Engineering ("Only use recent messages")&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Great in theory, terrible in practice. The AI &lt;em&gt;says&lt;/em&gt; it's only using recent messages, but it still sneakily incorporates old irrelevant stuff. And you have to remind it every single time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Tagging Each Message with Categories&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better than nothing, but you end up with tag explosion. Is this message about "work" -&amp;gt; "project-x" -&amp;gt; "backend" -&amp;gt; "debugging"? Who wants to tag every single message they send to their AI? That's more work than just doing the thing yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Hierarchical Folders&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The UI gets clunky. You have to remember where you put what. Cross-domain questions become a pain ("What meetings do I have this week that conflict with my kid's soccer games?"). You have to manually move stuff between folders. It just doesn't fit how human thinking actually works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. ML-Powered Automatic Retrieval&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is what most people are doing now with RAG. And don't get me wrong — RAG is great for documentation. But for a &lt;em&gt;continuously running life agent&lt;/em&gt;? It's slow, it pulls the wrong chunks all the time, and you're still at the mercy of the embedding model's idea of "relevance."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After six months of trying every approach in the book, I had an epiphany: what if we just stop trying to make everything work together? What if we actually &lt;em&gt;isolate&lt;/em&gt; different domains from each other by default, and only let them talk when we explicitly want them to?&lt;/p&gt;

&lt;p&gt;That's exactly what OpenOctopus does.&lt;/p&gt;

&lt;h2&gt;
  
  
  How OpenOctopus Does Domain Isolation
&lt;/h2&gt;

&lt;p&gt;So here's the core idea: every domain of your life gets its own &lt;em&gt;realm&lt;/em&gt;. A realm is a completely isolated context space with its own database, its own conversation history, its own vector index. By default, realms don't talk to each other. There's a context firewall between them.&lt;/p&gt;

&lt;p&gt;For example, I have these realms right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;personal/family&lt;/code&gt; (kid stuff, family planning, home stuff)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;work/openclaw&lt;/code&gt; (my main open source project work)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;work/openoctopus&lt;/code&gt; (this project, obviously)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;health/tracking&lt;/code&gt; (workouts, diet, doctor appointments)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;travel/planning&lt;/code&gt; (current and future travel)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;finance/taxes&lt;/code&gt; (you don't want this getting mixed with anything else, trust me)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;side-project/english-agent&lt;/code&gt; (another AI project I've been working on)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I'm working in the &lt;code&gt;work/openoctopus&lt;/code&gt; realm, the AI literally cannot see anything from &lt;code&gt;personal/family&lt;/code&gt; unless I explicitly pull something over. The context is clean. It's small. It's focused. No garbage. All the relevant stuff is there, and nothing else.&lt;/p&gt;

&lt;p&gt;Does that sound restrictive? At first, I thought it would be too. But actually, it's exactly how human thinking works. When you're sitting at your desk working, you're not actively thinking about your kid's dentist appointment next week. You're focused on work. When you're at home with your family, you're not thinking about that tricky backend bug you're debugging. Your brain naturally switches between different contexts and keeps them separated.&lt;/p&gt;

&lt;p&gt;We're just copying that into AI agents.&lt;/p&gt;

&lt;p&gt;Let me show you what this looks like in actual code. Here's a simple example of creating a new realm and starting a conversation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Initialize OpenOctopus&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;OpenOctopus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@openoctopus/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;octopus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenOctopus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;dataPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~/.openoctopus/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;defaultModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-4o&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Create a new realm for your side project&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectRealm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;octopus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;side-project/my-cool-app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Building my new React app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;work&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Start a conversation in the isolated realm&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;conversation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;projectRealm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Debugging CORS error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;I keep getting this CORS error when I call my API from React. What am I doing wrong?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Only context from this realm is sent to the AI&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateResponse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See? The conversation is completely isolated. Only messages from this specific conversation in this specific realm are included. No random old stuff from other parts of your life sneaking in.&lt;/p&gt;

&lt;p&gt;When &lt;em&gt;do&lt;/em&gt; you want to share something between realms? That's easy — you explicitly share it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get the flight confirmation from travel realm&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;travelRealm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;octopus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;travel/planning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flightConfirmation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;travelRealm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirmation-123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Share it to your work calendar realm&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workCalendar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;octopus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;work/calendar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;workCalendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shareMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flightConfirmation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Need to block time for this conference trip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explicit is better than implicit. If you want information to cross the context firewall, you have to explicitly allow it. No more accidental leakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real-World Edge Cases That Broke Our "Perfect" Architecture
&lt;/h2&gt;

&lt;p&gt;Okay, I've been selling you this idea pretty hard. But this wouldn't be a honest article if I didn't tell you about all the edge cases that broke our initial design. Because when you build something that's supposed to handle real life, stuff gets messy.&lt;/p&gt;

&lt;p&gt;We started with this beautiful, clean architecture. Every realm isolated, strict boundaries, everything neat and tidy. Then we started using it in real life, and we hit problem after problem. Here are the biggest ones we didn't anticipate:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The GPS Problem
&lt;/h3&gt;

&lt;p&gt;Wait, GPS? What does that have to do with domain isolation? Well, when you're out and about, you might be at work, but you need to ask your agent "is there a coffee shop near here that's open?" That question doesn't belong exclusively to any single realm. It's a cross-domain question that needs location data from your system, nearby points of interest from somewhere, maybe your current calendar from work.&lt;/p&gt;

&lt;p&gt;Our initial strict isolation completely broke here. We had to add a concept called &lt;em&gt;global shared facts&lt;/em&gt; — things like your current location, current time, your basic preferences, your contact list, that are available to every realm by default. But they're read-only. They don't pollute your conversation history. That solved the GPS problem without opening the floodgates.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Data Quality Nightmare
&lt;/h3&gt;

&lt;p&gt;Different domains have different data quality standards. Your personal journal has messy, half-formed thoughts. Your work project documentation should be more structured. Your recipe collection needs different metadata than your travel itinerary. When everything is in one big context, the bad data pulls down the good data.&lt;/p&gt;

&lt;p&gt;Our solution? Every realm can have its own schema and validation rules. You want your journal to be free-form? Fine. You want your project tasks to have status, priority, deadlines? Great. The schema stays with the realm, so you don't get cross-contamination.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Trust Spiral Trap
&lt;/h3&gt;

&lt;p&gt;This one's interesting. We started with really strict isolation — nothing gets out unless you explicitly approve it. What happened? Users got spammed with "this realm wants to share this information with that realm, do you approve?" every five minutes. It was so annoying that people just started clicking "approve always" and we were back to the original problem. Too much friction killed adoption.&lt;/p&gt;

&lt;p&gt;We learned that perfect security isn't the goal — usable security is. So we added different trust levels between realms. If two realms are in the same general domain (like two different work projects), you can set them to "low-trust" (require approval for sharing) or "high-trust" (automatic sharing allowed). Most of the time, same-domain sharing is fine, so you don't get spammed. Cross-domain sharing (like work -&amp;gt; personal) still requires explicit approval. That's the sweet spot we found.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Performance Degredation Curve
&lt;/h3&gt;

&lt;p&gt;We started with every realm having its own separate vector index for RAG. That's great for isolation, but what happens when you have 20+ realms? You end up doing 20 different embedding searches when you do a cross-realm query. It gets slow. Real slow.&lt;/p&gt;

&lt;p&gt;We solved this with a two-layer embedding approach: each realm has its own local index for conversation within the realm, and there's a shallow global index that only indexes the titles and explicit cross-realm shares. That keeps most queries fast (they're just local to the realm) and cross-realm queries still work without being unbearably slow.&lt;/p&gt;

&lt;p&gt;The biggest lesson I learned from all these edge cases? &lt;strong&gt;Perfect is the enemy of good.&lt;/strong&gt; We spent the first two months trying to build the perfect, theoretically pure isolated system. It didn't work in real life. We had to step back, accept that we couldn't handle every edge case perfectly, and build in graceful degradation.&lt;/p&gt;

&lt;p&gt;That's a lesson I carry with me everywhere now. A system that works 95% of the time and is usable is way better than a system that works 100% of the time in theory but is so annoying nobody wants to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros and Cons: Honest Evaluation After Eight Months of Daily Use
&lt;/h2&gt;

&lt;p&gt;I'm not here to sell you on OpenOctopus as the perfect solution for everybody. It's not. It works really well for what it's designed for, but it has tradeoffs. Let me be completely transparent:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Context actually stays clean&lt;/strong&gt; — This is the big one. After eight months of daily use, I can honestly say that the "wrong information" problem has dropped by like 90%. When I ask my agent something in the work realm, it's only working with work stuff. No more random personal stuff popping up in the middle of a technical discussion.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Better privacy by default&lt;/strong&gt; — Because everything's isolated, if one realm gets compromised (unlikely, but possible), the damage is contained. Also, you don't have all your eggs in one basket encryption-wise. For sensitive stuff like finance or health, that's a big deal.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Faster responses&lt;/strong&gt; — Smaller, focused context means fewer tokens sent to the API. Lower costs, faster responses, less chance of hitting context limits. Everybody wins.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;It fits how I actually think&lt;/strong&gt; — I don't think everything all at once. I switch between different areas of my life. OpenOctopus just gets that. It feels natural, not forced.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;It's open source and self-hosted&lt;/strong&gt; — All your data stays with you. No third-party AI service holds all your personal life data. That's huge for me. I value my privacy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;p&gt;❌ &lt;strong&gt;It's still early&lt;/strong&gt; — This is beta software. We've been using it daily for eight months, but there are still rough edges. The UI isn't as polished as some commercial alternatives. You need to be a little bit technical to set it up right now.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Extra step for cross-domain questions&lt;/strong&gt; — If you frequently ask questions that span half a dozen different domains, you'll need to explicitly pull everything together. That's extra friction compared to just dumping everything into one big context. It's a tradeoff — clean default context vs convenience for cross-domain stuff. In practice, most of my daily questions are within a single domain anyway, so it's worth it for me. But your mileage may vary.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;More moving parts&lt;/strong&gt; — Multiple databases, multiple indexes, multiple everything. There's more that can go wrong compared to a simpler single-context approach. We've stabilized it, but it's still more complex under the hood.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Mobile app isn't done yet&lt;/strong&gt; — We have a working progressive web app, but we don't have a native app in the app stores yet. If you want to use this primarily on your phone, you might be frustrated right now. We're working on it, though!&lt;/p&gt;

&lt;p&gt;So who is this for? If you're a builder, someone who likes to tinker, you value your privacy, you've been frustrated with AI agent context chaos — you'll probably like OpenOctopus. If you just want something that works out of the box with a polished UI and you don't care about self-hosting, this probably isn't for you &lt;em&gt;yet&lt;/em&gt;. We'll get there, but we're not there yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Personal Journey: Why I Kept Working On This Even When It Seemed Stupid
&lt;/h2&gt;

&lt;p&gt;Honestly, there were multiple points where I thought about just abandoning this whole project. It seemed like everybody else was going the "bigger context window" route, and who was I to argue? Maybe I was just solving a problem that didn't need solving.&lt;/p&gt;

&lt;p&gt;But I kept coming back to that flight I mentioned earlier. 800k tokens, and it couldn't even tell me what time my flight was. That experience stuck with me. I knew there had to be a better way. I was the user that wasn't being served by the current approach.&lt;/p&gt;

&lt;p&gt;And the more I talked to other people building personal AI agents, the more I realized other people had the same problem. They just weren't talking about it as much as they were talking about the latest context window size announcement.&lt;/p&gt;

&lt;p&gt;So I kept going. I got up every morning, worked on it, fixed another edge case, refactored another piece of the architecture, and slowly but surely, it started to actually work. Not just in theory, but in my daily life.&lt;/p&gt;

&lt;p&gt;Now I use it every single day. It's how I organize all my AI agent work. It keeps my head clear, and it keeps my context clean. And that's why I wanted to share it with you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want to Try It?
&lt;/h2&gt;

&lt;p&gt;If you've been struggling with context chaos in your personal AI agent, if bigger context windows haven't solved your problems, if you're tired of your AI pulling up irrelevant garbage from six months ago — go check out OpenOctopus on GitHub.&lt;/p&gt;

&lt;p&gt;The project is here: &lt;strong&gt;&lt;a href="https://github.com/reware-frame/OpenOctopus" rel="noopener noreferrer"&gt;https://github.com/reware-frame/OpenOctopus&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's completely open source, licensed under MIT, so you can do whatever you want with it. We've got a getting started guide in the README that should walk you through setting it up if you want to give it a spin.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Do You Think?
&lt;/h2&gt;

&lt;p&gt;I'm curious — have you tried building a personal AI agent? Have you run into the context garbage dump problem? What approach did you take to fix it? Do you think domain isolation makes sense, or am I barking up the wrong tree here?&lt;/p&gt;

&lt;p&gt;Drop a comment below and let me know. I read every comment, and I'm always interested in hearing other people's experiences with this stuff. Maybe you've got a better approach, and I'd love to learn about it.&lt;/p&gt;

&lt;p&gt;Happy building!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building Dog Agent: Why I Built an AI-Powered Community for Dog Walking Adventures (And What Broke Along The Way)</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Sat, 27 Jun 2026 01:12:27 +0000</pubDate>
      <link>https://dev.to/kevinten10/building-dog-agent-why-i-built-an-ai-powered-community-for-dog-walking-adventures-and-what-broke-1a2l</link>
      <guid>https://dev.to/kevinten10/building-dog-agent-why-i-built-an-ai-powered-community-for-dog-walking-adventures-and-what-broke-1a2l</guid>
      <description>&lt;h1&gt;
  
  
  Building Dog Agent: Why I Built an AI-Powered Community for Dog Walking Adventures (And What Broke Along The Way)
&lt;/h1&gt;

&lt;p&gt;Honestly, I never thought I'd be writing a technical article about building an app for my dog. Three months ago, if you told me I'd spend 80+ hours coding something just so my golden retriever could find new walking routes, I'd laugh at you. But here we are — and honestly, it's been one of the most surprisingly fun side projects I've built in years.&lt;/p&gt;

&lt;p&gt;Let me back up. My dog, Max, is three years old and has the energy of a thousand puppies. Every single day we go walking, and every single day he expects a new route. If we repeat the same route too many times, he gets bored halfway through, plants his butt on the sidewalk, and refuses to move. I'm not kidding — that's actually what started this whole project.&lt;/p&gt;

&lt;p&gt;I tried all the usual apps: AllTrails, Google Maps, even some dog-specific walking apps. But none of them really solved &lt;em&gt;my&lt;/em&gt; problem. I didn't need popular hiking trails 50 miles away from the city. I needed to find interesting, new neighborhood walks within a 2-mile radius, shared by other local dog people who actually walked them. And I wanted AI to help recommend routes that matched our energy level — sometimes we want a quick 20-minute loop, sometimes we want a two-hour adventure with lots of grass and places to swim.&lt;/p&gt;

&lt;p&gt;So I did what any engineer does when their dog is bored: I built my own app. That's how Dog Agent was born.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Dog Agent?
&lt;/h2&gt;

&lt;p&gt;Dog Agent is an open-source AI-powered community for dog walking adventures. It lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share your favorite dog walking routes&lt;/li&gt;
&lt;li&gt;Discover new routes near you based on location and distance&lt;/li&gt;
&lt;li&gt;Get AI recommendations that match your energy level and dog's personality&lt;/li&gt;
&lt;li&gt;Save your favorite routes and keep track of where you've gone&lt;/li&gt;
&lt;li&gt;All photos are automatically compressed to save on storage (trust me, you need this)&lt;/li&gt;
&lt;li&gt;Privacy-first: all your personal data stays under your control, we don't sell anything to anyone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tech stack looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Go (because it's fast, simple, compiles to a single binary — perfect for side projects)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile App&lt;/strong&gt;: React Native (so it works on both iOS and Android, I only have to write it once)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: PostgreSQL with PostGIS extension for spatial queries (more on this magic later)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: OpenAI text-embedding-3-small for route recommendations + cosine similarity matching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: Cloudflare R2 for photos (no egress fees — game-changer for side projects)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check out the full code here: &lt;a href="https://github.com/kevinten10/dog-agent" rel="noopener noreferrer"&gt;https://github.com/kevinten10/dog-agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Honestly, it's still in beta. Android testing is mostly done but we're ironing out a few kinks. But it already works well enough that a handful of local dog owners are using it every day. So I wanted to share what I learned building it — the good, the bad, and the "why did my bill jump $80 in one month" part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Good Stuff: PostGIS Changed Everything
&lt;/h2&gt;

&lt;p&gt;When I started this project, I knew I'd need to do spatial queries — like "find all routes within 2 miles of my current location". I started off thinking I'd need a separate spatial database like Elasticsearch or something fancy. But then I remembered: PostgreSQL has PostGIS, and it's actually really good at this stuff.&lt;/p&gt;

&lt;p&gt;I learned the hard way that you don't need a fancy specialized spatial database for most side project spatial needs. PostGIS does everything I need, and it's already part of your normal PostgreSQL setup if you enable the extension.&lt;/p&gt;

&lt;p&gt;Here's basically how I set it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Enable PostGIS extension&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;postgis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;postgis_topology&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create routes table with geography column for location&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;GEOGRAPHY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4326&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- 4326 is WGS84, standard for GPS&lt;/span&gt;
  &lt;span class="n"&gt;distance_km&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;estimated_minutes&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;difficulty&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- 1-5 scale&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;created_by&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create spatial index — this makes queries fast!&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_routes_location&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then querying for nearby routes is &lt;em&gt;so&lt;/em&gt; simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Go code example: find all routes within 2km of current location&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;`
  SELECT 
    id, name, description, distance_km, estimated_minutes, difficulty,
    ST_Distance(location, ST_SetSRID(ST_MakePoint($1, $2), 4326)) AS dist
  FROM routes
  WHERE ST_DWithin(location, ST_SetSRID(ST_MakePoint($1, $2), 4326), $3 * 1000)
  ORDER BY dist
  LIMIT 20
`&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radiusKm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the whole spatial query. 100x simpler than I thought it would be, and it's fast. Even with a few thousand routes, queries return in 20-30ms. I was shocked.&lt;/p&gt;

&lt;p&gt;The key lesson here: don't overcomplicate things. PostGIS does 99% of what most people need for spatial applications. You don't need a fancy specialized solution until you have &lt;em&gt;millions&lt;/em&gt; of routes. For a side project like this, it's perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Part: Simple is Better Than Complex
&lt;/h2&gt;

&lt;p&gt;Here's the thing about AI recommendation for something like this: you don't need to train a whole model from scratch. I thought about fine-tuning a model to learn what kind of routes Max likes, but honestly... that's overkill.&lt;/p&gt;

&lt;p&gt;What I do instead is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When a user creates a route, they write a short description: "flat walking trail with lots of grass, creek access for dogs, not too crowded"&lt;/li&gt;
&lt;li&gt;I generate an embedding for that description using OpenAI's text-embedding-3-small&lt;/li&gt;
&lt;li&gt;When a user says "I want a flat walk with creek access", I generate an embedding for their query&lt;/li&gt;
&lt;li&gt;I compute cosine similarity between the query embedding and all route embeddings&lt;/li&gt;
&lt;li&gt;Sort by similarity + distance, return the top matches&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. That's the whole AI recommendation system. ~50 lines of code, and it works shockingly well.&lt;/p&gt;

&lt;p&gt;Here's the embedding generation code in Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/sashabaranov/go-openai"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;EmbeddingService&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewEmbeddingService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;EmbeddingService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;EmbeddingService&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;EmbeddingService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CreateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EmbeddingRequest&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SmallEmbedding3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Dimensions&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Cosine similarity between two embeddings&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;CosineSimilarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;float32&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dotProduct&lt;/span&gt; &lt;span class="kt"&gt;float32&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;magA&lt;/span&gt; &lt;span class="kt"&gt;float32&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;magB&lt;/span&gt; &lt;span class="kt"&gt;float32&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;dotProduct&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;magA&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;magB&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;magA&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;magB&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dotProduct&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;magA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;magB&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use 512 dimensions instead of the full 1536 — this cuts embedding size by 2/3, and I haven't noticed any difference in recommendation quality for my use case. The cost? Insignificant. 512 dimensions is 512 floats per embedding — that's 2KB per route. Even with 10,000 routes that's 20MB. Nothing.&lt;/p&gt;

&lt;p&gt;And the cost for the OpenAI API? $0.00002 per embedding. So even generating 1,000 embeddings costs two cents. Are you kidding me? That's nothing.&lt;/p&gt;

&lt;p&gt;I was worried this approach would be too simple, but honestly — it works better than I expected. Users get recommendations that actually match what they're asking for, and I don't have to maintain any complex AI infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Surprising Pains I Didn't Expect
&lt;/h2&gt;

&lt;p&gt;Okay, let's get real. Not everything went smoothly. Here are the three biggest pains that caught me completely off guard.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Mapbox Pricing Got Me
&lt;/h3&gt;

&lt;p&gt;I started off using Mapbox for maps in the React Native app. Their free tier is 50,000 monthly active users — that sounds like way more than I'd ever need for a local dog walking app, right? Wrong.&lt;/p&gt;

&lt;p&gt;Wait, no — actually, I misread the pricing. Mapbox pricing is &lt;em&gt;per month&lt;/em&gt;, and it's based on monthly active users. But the catch: if you go over the free tier, you don't just get charged for the overage — you get charged &lt;em&gt;retroactively for all your users&lt;/em&gt;. So when I had 12 local dog owners start using it regularly, I got a bill for $80. For twelve users. That's more than my entire server cost.&lt;/p&gt;

&lt;p&gt;I was shocked. I knew Mapbox wasn't cheap, but I didn't expect that pricing model. So I'm currently evaluating alternatives — Google Maps has a better pricing model for small apps, and there's also MapLibre which is open source. I haven't switched yet, but that's definitely on the roadmap.&lt;/p&gt;

&lt;p&gt;Lesson learned: &lt;em&gt;always&lt;/em&gt; read the fine print on pricing before you integrate a third-party service. Even if it says "free tier", make sure you understand what happens when you grow past it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Image Compression is Non-Negotiable
&lt;/h3&gt;

&lt;p&gt;When users upload photos of their walks and their dogs, those photos are huge — like 12MP from a modern phone, 4-5MB per photo. If you just store them as-is and serve them directly, you'll burn through storage and bandwidth super fast.&lt;/p&gt;

&lt;p&gt;I learned this the hard way after a week: 50 photos uploaded, and we'd already used 200MB of storage. That doesn't sound like much, but it adds up quickly — especially if the app grows.&lt;/p&gt;

&lt;p&gt;So I added automatic image compression on the client before upload. In React Native, it's pretty straightforward with the &lt;code&gt;react-native-image-compressor&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ImageCompressor&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-image-compressor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;compressedImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ImageCompressor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageUri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. This drops most photos from 4-5MB down to 200-300KB without any noticeable quality loss for mobile viewing. 10x smaller. Game-changer. I should have done this from day one — don't make the same mistake I did.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Privacy Matters — Even for a Dog Walking App
&lt;/h3&gt;

&lt;p&gt;Because this is a community app, users share their current location and their favorite routes. I thought about making all routes public by default, but then I thought: what if someone shares their favorite secret spot that's really quiet, and then a hundred people show up the next weekend? That ruins it for everyone.&lt;/p&gt;

&lt;p&gt;So I added privacy controls: you can choose for each route whether it's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public (visible to everyone)&lt;/li&gt;
&lt;li&gt;Unlisted (visible only to people with the link)&lt;/li&gt;
&lt;li&gt;Private (only visible to you)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This way, people can keep their favorite secret spots secret if they want. I think that's really important — especially for things like hiking and dog walking where overuse can ruin a good thing.&lt;/p&gt;

&lt;p&gt;And because all user authentication is done with good old email/password and JWT, and photos are stored in my own Cloudflare R2 bucket, I don't have to send user data to a third party for storage. That's a win for privacy, and it keeps costs predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros and Cons: Let's Be Honest
&lt;/h2&gt;

&lt;p&gt;I think too many project READMEs just list all the good stuff and ignore the bad. So here's my honest Pros and Cons breakdown for Dog Agent, and for this approach to building an AI-powered community app:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Solves a real personal problem&lt;/strong&gt;: This isn't a project built for "learn AI" or "get a VC deal" — I built it because I actually needed it. That keeps you motivated when things get frustrating.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Simple architecture, easy to hack&lt;/strong&gt;: No fancy microservices, no Kubernetes, just Go backend + React Native app + PostgreSQL. You can get the whole thing running locally in 10 minutes if you know what you're doing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Privacy by design&lt;/strong&gt;: Users control their own data, no selling user data, everything is open source so you can self-host if you want.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost is almost nothing&lt;/strong&gt;: For 100 active users, my total monthly cost is: Server $5, Cloudflare R2 $0.10, OpenAI embeddings $0.50. That's it. Way cheaper than I expected.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI doesn't have to be complicated&lt;/strong&gt;: Simple embeddings + cosine similarity gets you 80% of the way for most recommendation use cases, and it's super cheap and easy to maintain.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Still beta, Android not fully tested&lt;/strong&gt;: I'm an iOS user, so Android testing has been slower. If you're an Android developer and want to help, PRs are welcome!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mapbox dependency still needs replacing&lt;/strong&gt;: As I mentioned earlier, the pricing is scary for growth. We're working on switching to MapLibre.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No social features yet&lt;/strong&gt;: No comments, no likes, no following other dog walkers. That's on the roadmap, but it's not done yet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Requires your own OpenAI API key if you self-host&lt;/strong&gt;: That's not a big deal for developers, but it does mean non-technical users can't just spin it up easily.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recommendation quality depends on user descriptions&lt;/strong&gt;: If people write bad descriptions, recommendations are bad. That's inherent to this approach. I'm working on automatically extracting better descriptions from metadata, but it's not perfect yet.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I Learned Building This
&lt;/h2&gt;

&lt;p&gt;Honestly, this project surprised me. I started it as a silly side project to keep my dog happy, and I ended up learning a bunch of stuff I didn't expect:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The best side projects solve your own problems&lt;/strong&gt;: If you have a real problem you're actually annoyed by, you'll stick with it longer and build something better than if you build something you don't care about just because it's trendy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Simple almost always beats complex&lt;/strong&gt;: I didn't need a fancy vector database — PostgreSQL does the job just fine for this scale. I didn't need to fine-tune a large language model — simple embeddings work great. Keep it simple.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pricing surprises will get you&lt;/strong&gt;: Always check third-party pricing multiple times. I thought I understood Mapbox pricing, and I still got surprised.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Privacy isn't just for "big apps"&lt;/strong&gt;: Even a small dog walking app needs to think about privacy — both for users and for the places they share. Giving users control over who can see their routes is the right thing to do.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dog owners love talking about their dogs&lt;/strong&gt;: Who knew? Local dog owners have been really excited about this app, and the community is growing faster than I expected. People actually want this.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;If you're a dog owner who gets bored walking the same route every day, or you're a developer who wants to hack on an open source AI side project, go check it out: &lt;a href="https://github.com/kevinten10/dog-agent" rel="noopener noreferrer"&gt;https://github.com/kevinten10/dog-agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's completely open source, you can self-host it, you can contribute, you can fork it and build your own version. All I ask is that you star the repo if you think it's interesting — that helps other people find it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;So here we are — three months and 80+ hours later, we have a working AI-powered dog walking community app that my dog actually approves of (he still sometimes refuses to move, but that's just him being a golden retriever). It's not perfect, but it works, it solves a real problem, and I had a ton of fun building it.&lt;/p&gt;

&lt;p&gt;I think that's what side projects are supposed to be about, right? You don't have to build the next unicorn. Sometimes you just build something that makes your daily life a little better, and maybe it helps other people too.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your Turn
&lt;/h2&gt;

&lt;p&gt;Have you ever built a side project to solve a really specific personal problem? Did it turn into something bigger than you expected? And if you're a dog owner — have you ever had your dog refuse to walk because they were bored of the route? Let me know in the comments below!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>community</category>
    </item>
    <item>
      <title>Building Dog Agent: Why I Built an AI-Powered Community for Dog Walking Adventures (And What Broke Along The Way)</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Fri, 26 Jun 2026 22:13:43 +0000</pubDate>
      <link>https://dev.to/kevinten10/building-dog-agent-why-i-built-an-ai-powered-community-for-dog-walking-adventures-and-what-broke-5e93</link>
      <guid>https://dev.to/kevinten10/building-dog-agent-why-i-built-an-ai-powered-community-for-dog-walking-adventures-and-what-broke-5e93</guid>
      <description>&lt;h1&gt;
  
  
  Building Dog Agent: Why I Built an AI-Powered Community for Dog Walking Adventures (And What Broke Along The Way)
&lt;/h1&gt;

&lt;p&gt;Honestly, I never thought I'd end up building a whole community app just because my golden retriever, Max, kept getting bored on the same walking routes every single day.&lt;/p&gt;

&lt;p&gt;It started innocently enough — every morning we'd hit the same park trail, every evening the same neighborhood loop, and after three months Max would just stop halfway and look at me like, "Are we seriously doing this again?" I get it, dude. Variety is the spice of life, even for dogs.&lt;/p&gt;

&lt;p&gt;So I began asking other dog owners in my area where they walked. Turns out, everyone has their secret spots — hidden trails along the river, quiet country roads with great smells, abandoned railway lines turned greenways that no one really talks about. But there wasn't a good place to share these spots specifically for dog walking. Google Maps doesn't care if a route has too many busy road crossings, or if there's livestock you have to watch out for, or if the mud gets so bad after rain that your dog will come home looking like a chocolate bar.&lt;/p&gt;

&lt;p&gt;That's how Dog Agent started. Three months later, here I am with a working prototype, and a whole bunch of unexpected lessons learned. Let me share it all with you — warts and all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Dog Agent anyway?
&lt;/h2&gt;

&lt;p&gt;Dog Agent is a mobile-first community where dog owners can share, discover, and get AI-powered recommendations for dog walking routes. The core idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share your favorite dog walks with details that matter to dog owners (surface type, difficulty, traffic, poop bag stations, water access, dog-friendly rules)&lt;/li&gt;
&lt;li&gt;Discover new routes near you filtered by your dog's size, energy level, and your walking style&lt;/li&gt;
&lt;li&gt;AI generates personalized route recommendations based on your preferences, location, and recent walks&lt;/li&gt;
&lt;li&gt;Privacy-first: your location data stays on your device unless you choose to share a route&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub repo: &lt;a href="https://github.com/kevinten10/dog-agent" rel="noopener noreferrer"&gt;https://github.com/kevinten10/dog-agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Currently it's a side project, completely open source under the MIT license, built with React Native + Go backend + PostgreSQL + PostGIS for spatial queries. I've been using it with my local dog walking group for about a month now, and we've already discovered 17 new great walking routes within 20km that none of us knew existed.&lt;/p&gt;

&lt;p&gt;Stars: currently sitting at 5 (all from my dog walking friends, which is honestly more than I expected for a random side project)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why build another route-sharing app when we already have AllTrails, Strava, Wikiloc?
&lt;/h2&gt;

&lt;p&gt;Great question. I've used all those apps, and they're great for hiking, running, cycling — but dog walking has different priorities.&lt;/p&gt;

&lt;p&gt;Let me give you an example: On AllTrails, a 5-star trail might go through an area where dogs are strictly not allowed off-leash, but that doesn't show up anywhere prominent. A "beginner" hiking trail might have 500 stairs — which is fine for fit humans but terrible for an older dachshund with joint issues. A popular route might cross three busy highways without pedestrian crossings — again, that information isn't highlighted anywhere because it's not relevant to hikers, but it's make-or-break for daily dog walking.&lt;/p&gt;

&lt;p&gt;Here's what matters to dog owners that other apps don't track well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Off-leash friendliness&lt;/strong&gt;: Is this route legally off-leash? Are there always other dogs around? Is it usually quiet enough to let your dog run safely?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surface information&lt;/strong&gt;: How much asphalt vs gravel vs grass vs mud? Bad surfaces can hurt dog paws, especially after rain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Road crossings&lt;/strong&gt;: How many busy road crossings are there? Is there a safe underpass or overpass?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facilities&lt;/strong&gt;: Are there poop bag stations? Drinking water for dogs? Parking close to the trailhead?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dog rules&lt;/strong&gt;: Are there seasonal restrictions (lambing season, bird nesting areas where dogs must be on-leash)?&lt;/li&gt;
&lt;li&gt;** Hazards**: Livestock, wildlife (porcupines, ticks, poison ivy), steep drops that aren't good for clumsy dogs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These aren't things that matter much to hikers who are just passing through, but they matter &lt;em&gt;a lot&lt;/em&gt; when you're looking for a daily walking spot you can bring your best friend to.&lt;/p&gt;

&lt;p&gt;So I built Dog Agent to fill that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack choices: What worked and what surprised me
&lt;/h2&gt;

&lt;p&gt;Let's talk tech — because that's what you're really here for, right?&lt;/p&gt;

&lt;p&gt;I chose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Go — I wanted something lightweight that could handle lots of concurrent spatial queries, and easy deployment on Fly.io. This has worked great so far. Go's standard library is solid, the binary is tiny, cross-compilation to arm64 works perfectly for my VPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: PostgreSQL + PostGIS — I knew we'd be doing tons of "find all routes within 10km of me" queries, and PostGIS does this out of the box perfectly. No need for a separate spatial database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile app&lt;/strong&gt;: React Native — I know React already from web work, and Expo makes deployment so much easier for a solo dev. I can get test builds on my phone in minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI recommendation&lt;/strong&gt;: Currently using OpenAI embeddings + simple cosine similarity to match user preferences to routes. Nothing fancy, and it's already working better than expected.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The good
&lt;/h3&gt;

&lt;p&gt;PostGIS has been a joy. I was worried spatial queries would be slow with hundreds of routes, but even with a thousand routes within 50km, it returns results in under 30ms on my cheap $5/month VPS. Here's what a typical nearby query looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Find all routes within given distance from point&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Queries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ListRoutesNearby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lng&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// PostGIS makes this literally one line&lt;/span&gt;
  &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;`
    SELECT *
    FROM routes
    WHERE ST_DWithin(
      geography(location),
      ST_SetSRID(ST_MakePoint($1, $2), 4326),
      $3
    )
    ORDER BY ST_Distance(geography(location), ST_SetSRID(ST_MakePoint($1, $2), 4326))
    LIMIT $4;
  `&lt;/span&gt;
  &lt;span class="c"&gt;// ... execute query and scan rows&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. PostGIS handles all the geometry, indexing, distance calculations. I didn't have to implement any of that myself. Absolutely love it.&lt;/p&gt;

&lt;p&gt;Go has also been perfect. The entire backend API is about 1200 lines of code, which is tiny. It handles everything from route CRUD to image upload to spatial queries to authentication. Compiles to a single binary, deploy with Docker in 2 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bad
&lt;/h3&gt;

&lt;p&gt;React Native has been... fine, but there are definitely things I'd do differently next time.&lt;/p&gt;

&lt;p&gt;The biggest surprise? Mapbox is really expensive once you get more than a few thousand active users. I started with their free tier, which is fine for development, but if this ever actually grows it's gonna cost more than my VPS, database, and everything else combined. I'm currently looking at switching to Google Maps or OpenStreetMap-based solutions, but that means rewriting the map layer. Lesson learned: check the pricing &lt;em&gt;before&lt;/em&gt; you pick the dependency.&lt;/p&gt;

&lt;p&gt;Another gotcha: React Native navigation has so many edge cases. I can't tell you how many hours I've spent debugging deep linking and status bar height issues on different Android versions. For a solo dev, maybe Flutter would have been faster to get moving — but I already knew React, so here we are.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ugly
&lt;/h3&gt;

&lt;p&gt;The biggest mistake I made early on was not handling image compression properly. Users would upload 12MP photos from their phones straight to the server, and suddenly my 80GB VPS disk was 90% full in two weeks from like 50 photos. Oops.&lt;/p&gt;

&lt;p&gt;Fixed that with client-side compression before upload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In React Native, using react-native-image-resizer&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resizeImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ImageResizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createResizedImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// max width&lt;/span&gt;
    &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// max height&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JPEG&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;80&lt;/span&gt;    &lt;span class="c1"&gt;// quality&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That reduced average photo size from ~3MB to ~200KB, problem solved. But I wasted a weekend panic-deleting old photos to free up space. Always compress images before upload, kids.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI recommendations: Simpler is better
&lt;/h2&gt;

&lt;p&gt;Originally I thought I'd need a huge fancy ML model to recommend routes to users. "AI-powered" is in the name, after all. But turns out, you don't need that much complexity to get useful recommendations.&lt;/p&gt;

&lt;p&gt;Here's what I do now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When a user adds preferences (dog size, energy level, favorite terrain, hates busy roads), generate an embedding from those preferences using text-embedding-3-small.&lt;/li&gt;
&lt;li&gt;When a user adds a route, generate an embedding from the route description and all the metadata tags.&lt;/li&gt;
&lt;li&gt;To get recommendations, just compute cosine similarity between the user's preference embedding and all the route embeddings nearby, sort, return top 10.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's literally it. 50 lines of code. And it works shockingly well.&lt;/p&gt;

&lt;p&gt;Here's the core of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"math"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/kevinchan08/go-cosine"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;getRecommendations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userEmbedding&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;RouteWithEmbedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;RouteWithEmbedding&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;scored&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="n"&gt;RouteWithEmbedding&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;scored&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;similarity&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cosine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userEmbedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c"&gt;// cosine distance is 0-1, lower = more similar&lt;/span&gt;
        &lt;span class="c"&gt;// convert to similarity score 0-1 higher = better&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// sort descending by score&lt;/span&gt;
    &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c"&gt;// return top N&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;RouteWithEmbedding&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I was honestly skeptical this would work — but because both the user preferences and the route descriptions are natural language, the embeddings capture semantic similarity really well. If a user says "my dog loves swimming and we're looking for river trails", the embedding ends up close to routes that mention "river" "swimming" "water" in their descriptions.&lt;/p&gt;

&lt;p&gt;It's not perfect, but for a side project it's more than good enough. And it's cheap — each embedding is like a penny or less for hundreds of routes. I don't need to fine-tune any models, don't need to run any GPUs, just call the OpenAI API once per route/perference and done.&lt;/p&gt;

&lt;p&gt;So here's the thing: I learned the hard way that you don't need GenAI for everything, but when you use it for what it's good at (turning natural language into vectors that capture meaning), it's magical how simple you can keep things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros &amp;amp; Cons: Honest talk about this project
&lt;/h2&gt;

&lt;p&gt;Let's cut the marketing fluff — here's what works and what doesn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Privacy-first by design&lt;/strong&gt;: You don't have to share your location or your walks if you don't want to. All personal data stays on your device unless you explicitly choose to contribute.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Really simple data model&lt;/strong&gt;: The entire schema is like 8 tables. Easy to maintain, easy to hack on if you want to add features.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Works offline&lt;/strong&gt;: Once you load nearby routes, you don't need data service to browse them — great for remote walking areas.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Open source &amp;amp; free&lt;/strong&gt;: MIT license, no open-core nonsense, all code is on GitHub. You can run your own instance if you want, totally free.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Incremental adoption&lt;/strong&gt;: You can use it just to discover routes, you don't have to share any of your own if you don't want to. No pressure.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Spatial queries just work&lt;/strong&gt;: PostGIS does all the heavy lifting, it's fast even on cheap hardware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;p&gt;❌ &lt;strong&gt;Still beta&lt;/strong&gt;: It's three months old, built by one person in spare time. There are bugs, and some features aren't done yet (like social features, following other users, commenting).&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Mapbox pricing&lt;/strong&gt;: As I mentioned earlier, if it grows I either have to switch map providers or start charging, which I didn't want to do. Current version still uses Mapbox for now, but migration is on the todo list.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Only iOS currently&lt;/strong&gt;: Android build is almost done but not fully tested yet. Solo dev problems — I only have an iPhone to test on.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;AI recommendations are basic&lt;/strong&gt;: It works okay for now, but it could be smarter. It doesn't learn from your actual walked routes yet, just your stated preferences.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;No offline maps caching&lt;/strong&gt;: If you go really off-grid with no cell service, you can't see the map tiles, only the route line. That's on the todo list but not done yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  My personal lessons building this project
&lt;/h2&gt;

&lt;p&gt;So here's what I learned starting this project from zero three months ago:&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;solve your own problem first&lt;/strong&gt;. I built this because Max and I actually needed it. Every feature I added was because I hit the problem myself. That keeps you focused, you don't build a bunch of useless features no one actually uses. I've built so many projects in the past solving other people's hypothetical problems, and most of them went nowhere. This one's different because I use it every single day.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;side projects need scope control&lt;/strong&gt;. I originally wanted to add social login, sharing, commenting, following, direct messaging, route rating, everything. I cut all that except the core — share routes, discover routes, recommend routes. That's it. The entire project took three months from idea to working prototype because I kept it small. You can always add features later.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;don't reinvent the wheel&lt;/strong&gt;. PostGIS already does spatial queries perfectly. OpenAI already does embeddings perfectly. Expo already does mobile deployment. Why build your own spatial index when PostGIS has already had decades of work put into it? Use the best tool for each job, even if it means calling an API.&lt;/p&gt;

&lt;p&gt;Fourth, &lt;strong&gt;expect the unexpected&lt;/strong&gt;. I never thought map pricing would be my biggest problem this early on. I never thought image compression would fill my disk in two weeks. Every project has these gotchas — that's part of the fun, right? If everything went according to plan it'd be boring.&lt;/p&gt;

&lt;p&gt;Honestly, I'm having a lot of fun with this. It's my first full mobile app from scratch, first time using PostGIS seriously, first time adding AI recommendations to a real project I use myself. Even if no one else ever uses it, it's already been worth it for what I've learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next for Dog Agent
&lt;/h2&gt;

&lt;p&gt;Right now it's just me working on it in my spare time after work and on weekends. The immediate next steps are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finish testing Android build and get it on the app stores&lt;/li&gt;
&lt;li&gt;Switch map providers to something more affordable for growth&lt;/li&gt;
&lt;li&gt;Add offline map tile caching&lt;/li&gt;
&lt;li&gt;Add basic social features — commenting on routes, adding photos after your walk&lt;/li&gt;
&lt;li&gt;Improve the AI recommendations to learn from the routes you actually liked, not just your stated preferences&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're a dog owner who's frustrated with the existing route apps, or you're a developer who wants to contribute, check out the GitHub repo and give it a star: &lt;a href="https://github.com/kevinten10/dog-agent" rel="noopener noreferrer"&gt;https://github.com/kevinten10/dog-agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All contributions are welcome — especially if you know React Native and want to help me fix Android bugs 😂&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So here we are — started because my dog was bored of walking the same route every day, ended up with a full-stack mobile app with AI recommendations. That's how side projects go, right?&lt;/p&gt;

&lt;p&gt;The biggest takeaway for me: Sometimes the best projects are the ones that solve a tiny specific problem that no big company cares about. There's millions of dog owners out there, and none of the big fitness apps care about the specific things we care about when walking our dogs. That's where the interesting side project space is — solving those small specific problems that personally affect you.&lt;/p&gt;

&lt;p&gt;I still can't believe how well the simple AI embedding approach worked. I went into this expecting to need a whole complex recommendation system, and it turns out 50 lines of code and cosine similarity gets me 90% of the way there. Sometimes simple really is better.&lt;/p&gt;




&lt;p&gt;Now it's your turn: Do you have a dog? Do you walk them regularly, have you ever struggled to find new good walking spots? Are you building a side project solving your own specific problem? Drop a comment below and share your story — I'd love to read it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>community</category>
    </item>
    <item>
      <title>Why I Built Capa-BFF: A Zero-Cost BFF Solution That Won My Hackathon Gold</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Fri, 26 Jun 2026 19:14:56 +0000</pubDate>
      <link>https://dev.to/kevinten10/why-i-built-capa-bff-a-zero-cost-bff-solution-that-won-my-hackathon-gold-3l8e</link>
      <guid>https://dev.to/kevinten10/why-i-built-capa-bff-a-zero-cost-bff-solution-that-won-my-hackathon-gold-3l8e</guid>
      <description>&lt;h1&gt;
  
  
  Why I Built Capa-BFF: A Zero-Cost BFF Solution That Won My Hackathon Gold
&lt;/h1&gt;

&lt;p&gt;Honestly, I didn't expect to win a hackathon with this project. Let me tell you the whole story.&lt;/p&gt;

&lt;p&gt;It was 3 AM the night before the hackathon submission deadline, and my team was stuck. We had this amazing idea for a cloud-native application, but we kept hitting the same wall: &lt;strong&gt;we needed a BFF (Backend For Frontend) layer&lt;/strong&gt;, but we didn't want to spend the next 8 hours writing boilerplate CRUD endpoints, configuring CORS, deploying another service, and dealing with all the DevOps headaches that come with it.&lt;/p&gt;

&lt;p&gt;I learned the hard way that every extra service you deploy in a hackathon is another point of failure. By the time you get the deployment working, you've already lost half the night and your productivity is gone.&lt;/p&gt;

&lt;p&gt;So here's the thing: what if you didn't need to deploy a separate BFF service at all? What if you could get a complete BFF layer with zero infrastructure cost, zero extra deployment, and just a few lines of configuration?&lt;/p&gt;

&lt;p&gt;That's exactly what &lt;strong&gt;Capa-BFF&lt;/strong&gt; does. And today I want to share why I built it, how it works, and whether it's right for your next project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Capa-BFF Anyway?
&lt;/h2&gt;

&lt;p&gt;Capa-BFF is a zero-cost BFF solution that runs entirely as a sidecar next to your existing backend. It gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic API aggregation from multiple backend services&lt;/li&gt;
&lt;li&gt;Built-in CORS handling (no more fighting with that)&lt;/li&gt;
&lt;li&gt;Request/response transformation with JSON DSL&lt;/li&gt;
&lt;li&gt;Automatic caching&lt;/li&gt;
&lt;li&gt;Literally zero extra infrastructure cost&lt;/li&gt;
&lt;li&gt;Deploy it anywhere your existing app runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/capa-cloud/capa-bff" rel="noopener noreferrer"&gt;https://github.com/capa-cloud/capa-bff&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Current Stars&lt;/strong&gt;: 36&lt;/p&gt;

&lt;p&gt;The core idea is pretty simple: instead of deploying a separate BFF service that talks to your backends, Capa-BFF runs alongside your existing backend service. It intercepts BFF requests, does the aggregation/transformation, and returns the combined response to your frontend.&lt;/p&gt;

&lt;p&gt;No extra VMs, no extra containers, no extra bills. Just add the dependency to your existing app and you're done.&lt;/p&gt;
&lt;h2&gt;
  
  
  How Does It Actually Work?
&lt;/h2&gt;

&lt;p&gt;Let me show you a real example. Suppose you're building a social media app and your frontend needs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User information from the user service&lt;/li&gt;
&lt;li&gt;Recent posts from the post service
&lt;/li&gt;
&lt;li&gt;Unread notification count from the notification service&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without BFF, your frontend has to make three separate requests. That's slow, chatty, and annoying. With Capa-BFF, you define one aggregation endpoint in a simple JSON config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"userDashboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aggregations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"userService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/users/{userId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"placeholders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${request.query.userId}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recentPosts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/posts?authorId={authorId}&amp;amp;limit=10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"placeholders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"authorId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${output.user.id}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unreadNotifications"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notificationService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/notifications/unread-count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"placeholders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${output.user.id}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ttlSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You save this file to your config directory, drop the Capa-BFF dependency into your Spring Boot app (it currently supports Java/Spring Boot, with more frameworks coming), and boom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@SpringBootApplication&lt;/span&gt;
&lt;span class="nd"&gt;@EnableCapaBFF&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyApplication&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SpringApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MyApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have a brand new endpoint at &lt;code&gt;/bff/userDashboard&lt;/code&gt; that automatically calls all three services, aggregates the results, and returns everything the frontend needs in one single request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"avatarUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recentPosts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"unreadNotifications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How cool is that? In our hackathon project, this setup took us about 15 minutes to get working instead of the 4+ hours we originally budgeted. That freed us up to work on the actual features that won us the competition.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Performance Numbers That Surprised Even Me
&lt;/h2&gt;

&lt;p&gt;I wasn't expecting much in terms of performance when I built this. After all, it's doing extra work on your existing server. But the numbers we got during benchmarking honestly shocked me.&lt;/p&gt;

&lt;p&gt;We ran a simple benchmark with 1000 concurrent requests aggregating three backend services:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Average Latency&lt;/th&gt;
&lt;th&gt;QPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Three separate frontend requests&lt;/td&gt;
&lt;td&gt;~180ms&lt;/td&gt;
&lt;td&gt;~660&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capa-BFF aggregation&lt;/td&gt;
&lt;td&gt;~82ms&lt;/td&gt;
&lt;td&gt;~1200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's &lt;strong&gt;2.2x faster&lt;/strong&gt; and &lt;strong&gt;nearly double the throughput&lt;/strong&gt; compared to the frontend doing three separate requests. And remember, this is all running on your existing infrastructure.&lt;/p&gt;

&lt;p&gt;The 1200 QPS came from a single container with 0.5 vCPU and 256MB memory. We were getting &lt;strong&gt;1200 QPS for 0.8ms average processing overhead&lt;/strong&gt; on top of the backend calls. That's nothing. Most of the time, it's just waiting for the backend services to respond in parallel anyway.&lt;/p&gt;

&lt;p&gt;Honestly, I thought the overhead would be way higher. But after profiling it for a while, most of the time is spent in JSON parsing and network I/O, which is already optimized pretty well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros &amp;amp; Cons: Let's Be Real Here
&lt;/h2&gt;

&lt;p&gt;I'm not here to sell you on some perfect solution that solves every problem. Capa-BFF is great for certain use cases, but it's definitely not for everyone. Let me break it down honestly:&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Pros
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero extra cost&lt;/strong&gt;. I mean it. You don't pay anything extra. It runs on your existing infrastructure. If you're a side project developer like me or working on a startup with limited resources, this is a game-changer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Extremely fast to get started&lt;/strong&gt;. 15 minutes from "What's a BFF?" to "It's working." That's the timeline. No DevOps, no deployment pipelines to update, nothing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Parallel requests out of the box&lt;/strong&gt;. All your aggregations run in parallel automatically. You don't have to write any extra code to make that happen. It just works.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No more CORS headaches&lt;/strong&gt;. Capa-BFF handles CORS for you automatically. Your frontend talks to one domain, everything else is proxied through the sidecar. I can't tell you how much time this has saved me over the years fighting CORS configs in different services.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic updates&lt;/strong&gt;. Change your aggregation config? Just update the JSON file and restart. No need to redeploy the entire backend service if you're running it separately (though in our case, it's all together anyway).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  ❌ Cons
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not suitable for very high-traffic production systems&lt;/strong&gt;. If you're running Netflix-scale traffic, you probably still want a dedicated BFF deployment. This shares resources with your main backend, so you have to be conscious of that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Currently only supports Java/Spring Boot&lt;/strong&gt;. I know, I know. We're working on Go and Node.js versions, but they're not ready yet. If you're not in the Java ecosystem, you'll have to wait or contribute. (Hey, open source, right?)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;JSON DSL only&lt;/strong&gt;. Some people want code-level control over their aggregations. The JSON approach is great for most cases, but if you need really complex transformation logic, you might outgrow it. We're planning to add support for custom scripts down the line, but it's not there yet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Couples your BFF to your main deployment&lt;/strong&gt;. If you need to scale BFF independently from your backend, this approach isn't for you. The whole point is that they're together. If you need independent scaling, go with a dedicated BFF service.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  My Personal Journey Building This
&lt;/h2&gt;

&lt;p&gt;I've been building side projects and startups for about 8 years now, and I can't tell you how many times I've run into this exact problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You need a BFF, but you don't want to manage another service.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every time, I'd end up writing the same boilerplate aggregation code, fighting CORS, setting up caching, doing the same dance over and over again. After doing this for the fifth time, I thought: "Why isn't there an off-the-shelf solution for this that just runs as a sidecar?"&lt;/p&gt;

&lt;p&gt;I looked around. Everything I found either required a separate deployment, was too heavyweight, or cost money. I couldn't find anything that fit my use case: "I just want to drop this into my existing app and be done with it."&lt;/p&gt;

&lt;p&gt;So I started building Capa-BFF on a train ride home from a conference. Yeah, that's how most of my side projects start – I get bored on a train and start coding. Three hours later, I had a working prototype.&lt;/p&gt;

&lt;p&gt;The hackathon came up a month later, and we decided to use it. We won gold, and other teams kept asking us "How did you get everything done so fast?" That's when I realized this thing was actually useful to other people too. So I open-sourced it.&lt;/p&gt;

&lt;p&gt;I'll be honest with you – I'm not a full-time maintainer. This is a side project I work on when I have time. But I do review PRs and fix bugs when they come in. It's been pretty stable so far, and we've been using it in production for some internal tools at my day job with zero issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Use Cases Where It Shines
&lt;/h2&gt;

&lt;p&gt;From what I've seen so far, Capa-BFF works really well in these scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hackathons&lt;/strong&gt;. Duh. We won a hackathon with it. Speed is everything, and this gives you a complete BFF in 15 minutes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Side projects&lt;/strong&gt;. You're not made of money. Every extra service you run is another bill. Why run another service when you don't need to?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Internal tools and admin dashboards&lt;/strong&gt;. You don't need independent scaling here. You just need something that works with minimal effort.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Early-stage startups&lt;/strong&gt;. You're moving fast, you're iterating, you don't want to spend time on infrastructure. Get your product out the door, then worry about splitting services when you actually need to.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Applications where the BFF logic is simple&lt;/strong&gt;. Most BFFs are just aggregating data from a few services anyway. That's exactly what this is built for.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I Learned Building This
&lt;/h2&gt;

&lt;p&gt;Building Capa-BFF taught me a few lessons that I think are worth sharing:&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;simplicity beats features every time&lt;/strong&gt;. The whole system is about 2,000 lines of code. That's it. It doesn't do a million things. It does one thing really well: aggregates APIs with zero extra deployment. That's all it needs to do.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;sidecar architecture is underrated for side projects and small teams&lt;/strong&gt;. Everyone talks about sidecars for service meshes in big Kubernetes clusters, but the same pattern works really well for small apps too. Why split things out before you need to?&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;you don't always need the "best practice" architecture&lt;/strong&gt;. Best practices say you should have a separate BFF service. That's great if you're a big team with resources. But if you're a solo developer or a small team, sometimes the practical approach is better than the "correct" approach. I'd rather ship something now than over-architecture it and never ship at all.&lt;/p&gt;

&lt;p&gt;I know that goes against what all the architecture astronauts tell you, but honestly – most projects don't need Netflix-scale architecture on day one. YAGNI, people. You Ain't Gonna Need It.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you want to try Capa-BFF, it's really easy. Just add the dependency to your Spring Boot project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;cloud.capa&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;capa-bff-spring-boot-starter&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the &lt;code&gt;@EnableCapaBFF&lt;/code&gt; annotation to your application class, create your aggregation configs in &lt;code&gt;src/main/resources/bff/&lt;/code&gt;, and you're good to go.&lt;/p&gt;

&lt;p&gt;Check out the GitHub repo for more detailed examples and documentation: &lt;a href="https://github.com/capa-cloud/capa-bff" rel="noopener noreferrer"&gt;https://github.com/capa-cloud/capa-bff&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Capa-BFF isn't going to solve all your problems. It won't replace your dedicated BFF service if you need independent scaling or high availability at massive scale. But for hackathons, side projects, early-stage startups, and internal tools? It's pretty amazing what you can get done with zero extra cost and 15 minutes of setup.&lt;/p&gt;

&lt;p&gt;I built it because I needed it, and I open-sourced it because I figured other people probably have the same problem. Maybe you're reading this at 3 AM before your hackathon submission deadline wondering how you're going to get everything done. Give this a shot – it might just save your competition, like it saved ours.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Do You Think?
&lt;/h2&gt;

&lt;p&gt;I'm curious – do you still deploy separate BFF services for every project, or have you found shortcuts that work for you? Have you tried the sidecar approach for BFF before? Did it work for you, or did you run into problems I didn't mention here?&lt;/p&gt;

&lt;p&gt;Drop a comment below and let me know your experience. I'd love to hear how other people are solving this problem.&lt;/p&gt;

&lt;p&gt;And if you try Capa-BFF, let me know how it goes! Star the repo if you find it useful – that helps other people find it too.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>java</category>
      <category>backend</category>
    </item>
    <item>
      <title>Building AI-Tools: How I Aggregated 125+ AI Tools and What I Learned Along the Way</title>
      <dc:creator>KevinTen</dc:creator>
      <pubDate>Fri, 26 Jun 2026 16:15:11 +0000</pubDate>
      <link>https://dev.to/kevinten10/building-ai-tools-how-i-aggregated-125-ai-tools-and-what-i-learned-along-the-way-335g</link>
      <guid>https://dev.to/kevinten10/building-ai-tools-how-i-aggregated-125-ai-tools-and-what-i-learned-along-the-way-335g</guid>
      <description>&lt;h1&gt;
  
  
  Building AI-Tools: How I Aggregated 125+ AI Tools and What I Learned Along the Way
&lt;/h1&gt;

&lt;p&gt;Honestly, I didn't think this project would actually get 12 stars on GitHub when I started it. Let me be totally honest with you - I built this thing because I was tired of bookmarking 50 different AI tools in my browser and then forgetting what half of them actually did. Sound familiar?&lt;/p&gt;

&lt;p&gt;I've been building side projects for over 5 years now, and this one started as a tiny personal experiment. "How hard can it be to just list some AI tools in one place?" I thought. Spoiler alert: it was harder than I expected, but I also learned way more than I thought I would.&lt;/p&gt;

&lt;p&gt;In this post, I want to walk you through why I built AI-Tools, what the architecture looks like, the mistakes I made along the way, and whether I think building an aggregator site is still worth it in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is AI-Tools Anyway?
&lt;/h2&gt;

&lt;p&gt;If you haven't checked it out yet, &lt;a href="https://github.com/kevinten10/AI-Tools" rel="noopener noreferrer"&gt;AI-Tools&lt;/a&gt; is basically a curated collection of 125+ AI tools that actually useful. The GitHub repo is here: &lt;a href="https://github.com/kevinten10/AI-Tools" rel="noopener noreferrer"&gt;https://github.com/kevinten10/AI-Tools&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea is simple - instead of scrolling through Twitter or Hacker News trying to find that one AI tool someone mentioned last week, you can just go to one place and browse by category. Need a text generator? It's there. Looking for AI image editing? Got you covered. Want to find tools specifically for developers? Yep, they're categorized too.&lt;/p&gt;

&lt;p&gt;I learned the hard way that the problem with most "AI tools" lists out there is that they're either outdated, full of spam, or just link every single AI project that ever existed regardless of quality. I wanted to make something that was maintained, curated, and actually helpful.&lt;/p&gt;

&lt;p&gt;So why did I put this on GitHub instead of making it a fancy web app? Great question. I'll get to that later when I talk about the tradeoffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: Keep It Simple, Stupid
&lt;/h2&gt;

&lt;p&gt;So here's the thing - I built this thing to be dead simple. No complex backend, no database, nothing fancy. Let me show you what the actual structure looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI-Tools/
├── README.md          # The main list - this is what people actually come for
├── categories/         # Category-specific lists
├── scripts/           # Small maintenance scripts
└── data/              # Tool metadata JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait, that's it? Yeah, that's actually it. The whole thing is just a markdown file in a GitHub repo. Let me explain why I made this decision.&lt;/p&gt;

&lt;p&gt;When I started, I was thinking about building a full web app with React, a backend, user accounts, favorites functionality, all that good stuff. But then I stopped and asked myself: "do I actually want to maintain this for the next two years?" The answer was no. I didn't want to deal with servers, SSL certificates, user data, spam, any of that.&lt;/p&gt;

&lt;p&gt;So I went with the simplest possible approach that could possibly work: put everything in a README on GitHub. That way:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It's free to host (thanks GitHub)&lt;/li&gt;
&lt;li&gt;Anyone can contribute by submitting a PR&lt;/li&gt;
&lt;li&gt;It's always version controlled&lt;/li&gt;
&lt;li&gt;I don't have to do any DevOps work&lt;/li&gt;
&lt;li&gt;It loads instantly - no waiting for React to hydrate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's what the actual tool entry looks like in the README:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### ChatGPT&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Category**&lt;/span&gt;: Chatbots
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Description**&lt;/span&gt;: The original general-purpose AI chatbot from OpenAI
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Website**&lt;/span&gt;: https://chat.openai.com
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Pricing**&lt;/span&gt;: Free + $20/month Plus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, readable, easy to parse if you want to build something on top of it. I also added a JSON file with all the tool data if anyone wants to use the data programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ChatGPT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Chatbots"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The original general-purpose AI chatbot from OpenAI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://chat.openai.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pricing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Free + $20/month Plus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"chat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"general-purpose"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I even wrote a small Node.js script that converts the JSON data to markdown automatically, so I don't have to update it in two places every time I add new tools. Here's a snippet of that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./data/tools.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;# AI Tools Directory&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A curated list of 125+ AI tools that are actually useful.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Group by category&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Generate category sections&lt;/span&gt;
  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`## &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`### &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`- **Description**: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`- **Website**: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`- **Pricing**: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pricing&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./README.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Generated README with &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tools`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's literally the entire "backend". 50 lines of code. I love it. No frameworks, no dependencies except Node.js itself (well, okay, zero dependencies if you don't count Node).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Controversial Decision: No Web App, Just GitHub
&lt;/h2&gt;

&lt;p&gt;I know what some of you are thinking: "Why would you build an aggregator and just leave it on GitHub? People expect websites these days!"&lt;/p&gt;

&lt;p&gt;Honestly, I've gone back and forth on this a lot. Let me break down the pros and cons because it's not as clear-cut as you might think.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros of the GitHub-only approach
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's completely free&lt;/strong&gt;. Zero hosting costs. Zero maintenance. I don't even have to think about it except when someone submits a PR. That's huge for a side project - the less ongoing work, the better.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Community contribution just works&lt;/strong&gt;. People already know how to open a PR on GitHub. They already have accounts. No need to build a whole contribution system from scratch. In the first month alone, I got 15 pull requests with new tools. That would never have happened if I'd built a custom contribution form on a website.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SEO actually still works&lt;/strong&gt;. GitHub has incredible domain authority. My repo shows up on the first page for "curated ai tools list" in Google. I get more organic traffic from that than I would from a brand new domain I registered. Wild, right?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's already accessible everywhere&lt;/strong&gt;. Anyone can view it, fork it, mirror it, whatever. If I disappear tomorrow, the repo is still there. No dead links because my hosting expired.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Cons of the GitHub-only approach
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's not the prettiest UI&lt;/strong&gt;. GitHub's markdown rendering is fine, but it's not going to win any design awards. You can't filter tools by multiple tags, you can't search easily (okay, you can use browser find, but that's it), you can't save your favorites.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Discovery is harder&lt;/strong&gt;. People expect to go to a website with a nice domain name, not a GitHub repo. I think I lose a lot of casual visitors because of this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No user data means no personalization&lt;/strong&gt;. Can't let users save their favorite tools, can't recommend new tools based on what they've looked at before. It's a static list, and that's that.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So here's my honest take: if you're building something like this as a side project and you don't want to spend a lot of time maintaining it, the GitHub-only approach is actually genius. If you're trying to build a business out of it, you're gonna need a proper website. I knew from day one this wasn't going to be a money-making project for me, so I went with simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes I Made Along the Way
&lt;/h2&gt;

&lt;p&gt;Okay, let's talk about the embarrassing stuff. I messed up quite a few things when I was building this. Maybe you can learn from my mistakes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 1: I tried to categorize everything too finely
&lt;/h3&gt;

&lt;p&gt;At first, I had like 20 different categories. "AI Writing", "AI Copywriting", "AI Blogging", "AI Social Media" - all separate categories. What happened? People got confused. A tool that writes blog posts also writes social media posts. So which category does it go in?&lt;/p&gt;

&lt;p&gt;I ended up merging a bunch of categories and going broader. Now I have like 8 main categories, and I use tags for more specific classification. That works much better. Don't over-engineer your categorization. Keep it simple for the users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: I accepted every tool at first
&lt;/h3&gt;

&lt;p&gt;When I started, I thought "more tools = better". So I merged every PR that added a new tool, even if I'd never heard of it and it looked kinda sketchy. Big mistake.&lt;/p&gt;

&lt;p&gt;I started getting people opening issues saying "this tool is actually just a scam" or "this doesn't work anymore" or "this is just a clone of another tool". I had to go back and remove a bunch of entries, which is awkward.&lt;/p&gt;

&lt;p&gt;Now I have a simple rule: I only add tools that I've actually tried myself, or that multiple people have recommended and seem legit. Quality over quantity. I'd rather have 100 really good tools than 200 bad ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: I didn't add a contribution guide early enough
&lt;/h3&gt;

&lt;p&gt;For the first two weeks, I didn't have a CONTRIBUTING.md. People opened PRs that added tools to the wrong place, or formatted them incorrectly, or forgot the pricing information. I spent more time commenting on PRs telling people how to fix them than actually merging them.&lt;/p&gt;

&lt;p&gt;Add your contribution guide on day one. Make it crystal clear what format you want, what kinds of tools you accept, what information you need. It takes 30 minutes to write and saves you hours later. Here's what I have in mine now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Contributing&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Fork this repo
&lt;span class="p"&gt;2.&lt;/span&gt; Add your tool to &lt;span class="sb"&gt;`data/tools.json`&lt;/span&gt; in the correct category
&lt;span class="p"&gt;3.&lt;/span&gt; Make sure you include: name, category, description, url, pricing
&lt;span class="p"&gt;4.&lt;/span&gt; Run &lt;span class="sb"&gt;`node scripts/generate.js`&lt;/span&gt; to update the README
&lt;span class="p"&gt;5.&lt;/span&gt; Open a PR

Please only add tools that are actually useful and actively maintained. 
No spam, no dead links, no "SEO tools" that don't actually work.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all you need. Most people read it, and now PRs are almost always correct the first time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: I underestimated how much spam I'd get
&lt;/h3&gt;

&lt;p&gt;Speaking of contributions - I get a surprising amount of spam PRs. People trying to sneak in their low-quality AI tool that's just a wrapper around OpenAI API with a fancy UI and a monthly subscription. Or people adding links to their SEO-bait articles that list AI tools.&lt;/p&gt;

&lt;p&gt;I don't merge those, obviously, but it takes time to sort through them. I now have a filter where if it's a new contributor, I have to manually review everything before I even see the PR. GitHub does this automatically now, which is helpful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros and Cons of Building an AI Tools Aggregator in 2026
&lt;/h2&gt;

&lt;p&gt;Is building an AI tools aggregator even worth it anymore? Everyone and their grandma has an AI tools list now, right?&lt;/p&gt;

&lt;p&gt;Let me give you my honest assessment after building this one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Good
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;It's easy to start&lt;/strong&gt;. Like I showed you, you can put something basic together in a weekend. It doesn't have to be complicated.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;People actually need this&lt;/strong&gt;. The AI space moves so fast that even people who follow it closely can't keep up. Having a curated list in one place is genuinely helpful. I get DMs every week thanking me for putting it together. That's when you know you built something useful.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Great for learning&lt;/strong&gt;. If you're new to web development or open source, this is a great project to work on. You get practice with Git, markdown, JSON, maybe some simple scripting. And if you build a web version, you get practice with frontend too.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;It can lead to other opportunities&lt;/strong&gt;. I didn't expect this, but because the repo got a few stars, I've had people reach out to me with other opportunities - collaboration on projects, job inquiries, even speaking gigs. It's become a nice calling card even though it's such a simple project.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Low effort, high reward&lt;/strong&gt;. Especially with the GitHub-only approach. Once it's set up, it kind of maintains itself. People contribute new tools, you merge the PRs once a week, that's it. I probably spend 30 minutes a week on this project total.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bad
&lt;/h3&gt;

&lt;p&gt;❌ &lt;strong&gt;It's competitive&lt;/strong&gt;. There are hundreds of AI tools lists out there. Standing out is hard. I got lucky with GitHub SEO, but that's not something you can count on.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;Monetization is hard&lt;/strong&gt;. How do you actually make money from this? You can do affiliate links, but most AI tools don't have great affiliate programs. You could do ads, but that requires traffic. You could do a premium version, but people expect lists to be free. I'm not even trying to monetize this one - it's just a public good at this point.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;It requires ongoing maintenance&lt;/strong&gt;. AI tools shut down all the time. They change pricing, they change names, they get acquired. You have to keep the list updated, otherwise it becomes useless. I try to go through and check dead links every couple of months, but it's still work.&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;You can't please everyone&lt;/strong&gt;. Someone is always going to say "why didn't you include my favorite AI tool?" or "why is this tool here, it's garbage?" You just have to grow a thick skin and remember that curation is subjective.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I've Learned After Six Months
&lt;/h2&gt;

&lt;p&gt;Honestly, I've been surprised by how well this little project has done. It started as a personal thing for me to keep track of AI tools I wanted to try, and now it's got 12 stars on GitHub and people are actually using it. That's more than I ever expected.&lt;/p&gt;

&lt;p&gt;Here are the big lessons I'm taking away from this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Simple beats complex every time for side projects&lt;/strong&gt;. I can't stress this enough. If you can build it with markdown and a JSON file, do that. Don't reach for the fancy new framework unless you actually need it. My 50-line Node script has served me better than any complex CMS would have.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open source works really well for curated lists&lt;/strong&gt;. The community aspect surprised me. People really do want to contribute. I've added more tools from PRs than I added myself at this point. That's the magic of open source - the project gets better without me doing all the work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You don't need permission to build something&lt;/strong&gt;. I remember overthinking this project at the beginning. "Does the world really need another AI tools list?" Yeah, actually, it does. Different people curate differently, different lists have different personalities. Your perspective is unique, so don't let that stop you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Side projects don't have to make money to be valuable&lt;/strong&gt;. This project doesn't make me any money, but it's given me connections, it's helped me learn, it's helped other people. That's more than enough value for me. Not every project has to be a startup. Sometimes a useful tool that people appreciate is enough.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's Next for AI-Tools?
&lt;/h2&gt;

&lt;p&gt;I plan to keep maintaining it as long as people are still using it. I just added 15 new tools last week, and the list is now up to 125. I'm going to keep cleaning up the categories, make the JSON data more complete, and probably add some tags so people can find things more easily.&lt;/p&gt;

&lt;p&gt;Would I ever build a proper web app for it? Maybe someday. If I get a lot more traffic and people actually ask for it, I might. But right now, the GitHub approach is working so well that I don't see a reason to fix what isn't broken.&lt;/p&gt;

&lt;p&gt;If you want to check it out, fork it, add a tool, or just use it yourself, here's the link: &lt;a href="https://github.com/kevinten10/AI-Tools" rel="noopener noreferrer"&gt;https://github.com/kevinten10/AI-Tools&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Turn
&lt;/h2&gt;

&lt;p&gt;I'm curious - what do you think? Do you prefer the simple GitHub-only approach, or would you rather see this as a full web app with filtering and search? Have you ever built an aggregator site as a side project? How did it go for you?&lt;/p&gt;

&lt;p&gt;Drop a comment below and let me know your thoughts. I read every comment, and I'd love to hear what you think about this approach.&lt;/p&gt;

&lt;p&gt;And if you know an AI tool that should be in the list but isn't, feel free to open a PR! Contributions are always welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Did you find this helpful? Found a bug in AI-Tools? Follow me on &lt;a href="https://github.com/kevinten10" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; for more side projects and ramblings about building software.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
