DEV Community

KevinTen
KevinTen

Posted on

Why I Built Capa-Java: A Multi-Runtime SDK That Lets You "Write Once, Run Anywhere" In The Cloud Native Era

Why I Built Capa-Java: A Multi-Runtime SDK That Lets You "Write Once, Run Anywhere" In The Cloud Native Era

Honestly, I've been meaning to write about this project for years. But honestly? I kept putting it off because it's a "scary" topic — everyone's talking about Sidecar architecture these days, and here I am arguing for an SDK approach. Am I just behind the times?

So here's the thing: I built Capa-Java four years ago when Dapr was still pretty new, and after all this time, I still think it fills an important gap that no one else is really talking about. Today I want to share what it is, why I built it, when you should use it, and the hard lessons I learned along the way.

What is Capa-Java, really?

Let me start with a simple question: If you're working at an enterprise with hundreds or thousands of existing Java applications, how do you adopt Multi-Runtime architecture?

The Sidecar model that Dapr, Layotto, and all the cool kids are using looks great on paper. But when you actually think about migrating thousands of applications to it… well, let's just say that's a lot of work. You have to change deployment pipelines, add infrastructure, coordinate across teams — it's not a trivial lift.

That's where Capa-Java comes in.

Capa-Java is a Rich SDK Mode implementation of the Multi-Runtime API. It gives you the same standardized APIs that Dapr gives you — service invocation, state management, pub/sub, configuration, telemetry — but instead of running as a separate sidecar container, it runs inside your application process.

The core idea is simple:

Standard API + Pluggable Components = Write Once, Run Anywhere
Enter fullscreen mode Exit fullscreen mode

You write your code once against Capa's standard API, and then you can deploy to AWS, Alibaba Cloud, Kubernetes, Dapr, whatever — without changing your business code. The SPI implementation handles the adaptation.

Here's what that looks like in actual code:

import group.rxcloud.capa.rpc.CapaRpcClient;
import group.rxcloud.capa.rpc.CapaRpcClientBuilder;
import group.rxcloud.cloudruntimes.domain.core.invocation.HttpExtension;
import group.rxcloud.cloudruntimes.utils.TypeRef;
import reactor.core.publisher.Mono;

public class DemoRpcClient {

    // Identifier in Capa for the service you want to invoke
    private static final String SERVICE_APP_ID = "12345.helloworld";

    public static void main(String[] args) {
        // Build the client - automatically loads the SPI implementation
        // for your current runtime environment
        CapaRpcClient capaRpcClient = new CapaRpcClientBuilder().build();

        // Invoke a method on another service - looks simple right?
        Mono<byte[]> responseMono = capaRpcClient.invokeMethod(
            SERVICE_APP_ID, 
            "hello", 
            "hello", 
            HttpExtension.POST, 
            null, 
            TypeRef.BYTE_ARRAY
        );

        byte[] response = responseMono.block();
        System.out.println(new String(response));
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it. That's your entire service invocation code. The same code works on any platform that has an SPI implementation. Change the implementation JAR on your classpath, and you're running on a different cloud provider without changing a line of business code.

Pretty cool, huh?

But Wait… Isn't Sidecar The Future? Why Bother With SDK?

I get this question a lot. "Why would you use an SDK approach when Sidecar is clearly the future?"

Let me be clear: I think Sidecar is the future. I love the Dapr project, I've contributed to discussions in their GitHub, I think the Multi-Runtime architecture is the right direction for our industry.

But here's the problem that nobody talks about: Most enterprises can't just flip a switch and switch to Sidecar.

Think about it: You've got a huge Java estate built up over the past 10-15 years. Your teams are already stretched thin maintaining what you have. Someone comes along and says "Hey, we need to rewrite all your applications to use this new Sidecar architecture!" How excited do you think management is going to be about that? How quickly do you think that's going to happen?

The answer is: years. If ever.

That's why we built Capa. It's a transitional step. It lets you start adopting the Multi-Runtime API today, without having to do a full-blown Sidecar migration. You can adopt it incrementally, one service at a time. And when you do eventually move to Sidecar, you just swap out the SPI implementation — your business code stays the same.

Here's the original GitHub issue where this discussion happened in the Layotto project: Java SDK design: Can we reuse existing industry standards?. The idea came directly from community discussions about how to adopt Multi-Runtime incrementally in large Java estates.

This diagram from the README explains the architecture better than I can:

Capa Architecture

What Features Do You Get?

At this point, you're probably wondering what Capa actually gives you out of the box. Here's the current feature matrix:

Feature Description Status
RPC / Service Invocation Call services across platforms with a standard API ✅ Stable
Configuration Dynamic configuration management with standard API ✅ Stable
Publish/Subscribe Pub/Sub messaging across different cloud providers ✅ Stable
State Management Standard API for key-value state stores ✅ Stable
Telemetry Unified logging, metrics, and tracing ✅ Stable
Database/SQL Standard SQL database API ⚠️ Alpha
Schedule Scheduled tasks abstraction ⚠️ Alpha

All the core features are stable and used in production. The newer ones (database and schedule) are still in alpha, but the core API is there.

One of my favorite design decisions is that we kept the API definitions completely independent of the Capa project. If you go look at cloud-runtimes-jvm, that's where all the API definitions live. This isn't "our proprietary API" — it's kept independent so it can become a community standard. We keep it synchronized with Dapr's API, following upstream community standards.

Why is this important? Because right now, Dapr's API is strongly coupled to the Dapr project. We want to see this API become a true community standard that any project can use, whether you're doing Sidecar or SDK. That's why we keep it independent.

Getting Started In 2 Minutes

If you're using Maven, it's literally two dependencies to get started. Here's what you add to your pom.xml:

<project>
    <dependencies>
        <!-- Core Capa SDK with all standard APIs -->
        <dependency>
            <groupId>group.rxcloud</groupId>
            <artifactId>capa-sdk</artifactId>
            <version>1.0.7.RELEASE</version>
        </dependency>
        <!-- SPI implementation for your target environment - this example uses the demo implementation -->
        <dependency>
            <groupId>group.rxcloud</groupId>
            <artifactId>capa-sdk-spi-demo</artifactId>
            <version>1.0.7.RELEASE</version>
        </dependency>
    </dependencies>
</project>
Enter fullscreen mode Exit fullscreen mode

That's it. Really. The demo implementation is great for learning and testing. For production, you just swap out the demo SPI with the SPI implementation for your target environment:

The business code doesn't change at all. Only the SPI implementation dependency changes. That's the magic of it.

I learned the hard way that incremental adoption is everything. If you can't get teams to adopt it because it requires too much change up front, the best architecture in the world doesn't matter. Capa is designed to be adopted incrementally — you can add it to an existing service in an afternoon.

Pros and Cons: Let's Be Honest

I know you're reading this thinking "Okay, this sounds good, but what's the catch?" Let me cut through the marketing and give you the real Pros and Cons based on four years of using this in production.

Pros ✅

  1. Incremental adoption — This is the big one. You don't need to "boil the ocean". Add Capa to one service, see if you like it, then keep going. Perfect for large enterprises with existing Java estates.

  2. No extra infrastructure — You don't need to run a sidecar container next to every pod. That means less operational complexity, less resource consumption, less things that can go wrong. For small projects or teams that can't afford the Sidecar overhead, this is a big win.

  3. Same API, everywhere — Once your team learns the Capa API, they can use it everywhere, regardless of which cloud or runtime you're on. Standardized API means less context switching, fewer platform-specific bugs.

  4. Reactor native — We built it from the ground up with Project Reactor for asynchronous non-blocking IO. You get all the benefits of reactive programming if you want it, but you can just call .block() if you want to do synchronous calls. Best of both worlds.

  5. Low migration cost — If you eventually want to move to Sidecar, you just swap the SPI implementation. Your business code stays 100% the same. No rewrites required. You've already won because you've standardized on the API.

  6. Community-standard API — We didn't invent our own proprietary API. We follow Dapr's API standard, kept in an independent repository. It's not "Capa lock-in" — it's industry-standard API.

Cons ❌

  1. Not pure Sidecar architecture — If you believe Sidecar is the one true way and you're starting greenfield, Capa probably isn't for you. The whole point of Capa is to be a transitional step for existing systems. It doesn't give you all the separation of concerns that Sidecar gives you.

  2. Language-bound — Since it's an SDK, it's language-specific. Right now we have great Java support, alpha support for Python and Go. Sidecar works with any language. This is a fundamental trade-off of the SDK approach.

  3. SPI implementations need maintenance — Each cloud/runtime needs its own SPI implementation. We have the main ones covered, but if you're using some obscure cloud provider, you'll need to write the SPI implementation yourself. It's not hard (it's just implementing a few interfaces), but it's work.

  4. Process colocation — Your application shares resources with the Capa runtime. If Capa has a memory leak or crashes, it takes your application down with it. In Sidecar architecture, if the sidecar dies, your application is still running (though it can't make calls, obviously). This is another fundamental trade-off.

Honestly, that last point is the biggest downside. I'm not going to sugarcoat it. When everything is in the same process, a bug in one place can take everything down. But for most existing Java applications, you're already dealing with that — you've got dozens of dependencies in your process already. Adding Capa isn't changing that fundamental model.

Real-World Lessons I Learned Building This

I started this project in 2022. That's four years ago as of this writing. I've learned a lot along the way. Here are the big ones:

Lesson 1: The industry moves slower than you think

I thought when we built this, enterprises would be jumping to adopt Sidecar within a couple years. That hasn't happened. It's been four years, and most enterprises are still in the "experimenting with Dapr on side projects" phase. That's not a criticism — change is hard. The point is, transitional solutions like Capa are still relevant way longer than I expected.

Lesson 2: Standardization is more important than perfection

Early on, I spent a lot of time trying to make the "perfect" API. What I learned is that having any standard API that everyone can agree on is more important than having the perfect API. Even if the API isn't perfect, standardization still gives you value because everyone speaks the same language.

Lesson 3: Incremental wins beat big bang rewrites

Every big bang rewrite I've ever seen has either failed or taken way longer than expected. Incremental migration is the only way to successfully adopt new architecture in large organizations. Capa was designed for that, and that design choice has been vindicated over and over.

Lesson 4: Separate API from implementation was the right call

Putting the API definitions in a completely independent repository was one of the best decisions we made. It means multiple projects can use the same API, collaborate on improvements, and it doesn't belong to any one project. That's how you build community standards.

When Should You Use Capa-Java?

Let me make this simple for you:

Use Capa-Java if…

  • You have an existing Java application portfolio and you want to start adopting Multi-Runtime APIs without a full Sidecar migration
  • You're working at an enterprise where change moves slowly and incremental adoption is the only realistic option
  • You have applications running on multiple clouds and you want a single consistent API across all of them
  • You're building a new Java project and you don't want the operational overhead of Sidecar yet

Don't use Capa-Java if…

  • You're starting a brand new greenfield project and can adopt Sidecar from day one
  • You need polyglot support (multiple languages in your system) and want the Sidecar model
  • You already have Dapr/Layotto running in production and you're happy with it
  • You believe strongly in the Sidecar architecture and don't need a transitional step

That's it. It's not about being "better" than Sidecar. It's about being complementary to Sidecar. It fills a different niche.

Where Is The Project Going?

We've been running Capa in production for several years now, and it's been working well. But there's still a lot to do:

  1. Finish the alpha features — Database and Schedule need to be stabilized. We've been working on this slowly between other projects.

  2. More SPI implementations — We have the main clouds covered, but it would be great to see community contributions for other platforms like GCP, Azure, etc.

  3. Better documentation — Like all open source projects, we could always use better documentation and more examples.

  4. Performance optimizations — It's already fast enough for most use cases, but there's always room for improvement.

If you're interested in contributing, we'd love to have you. Check out the GitHub repository and open an issue or a pull request.

Wrapping Up

Honestly, I didn't know if this project would still be relevant four years later. But the thing is, the problem it solves — incremental adoption of Multi-Runtime in large Java estates — is still a problem that hasn't been solved by anyone else.

The core insight still holds: Sidecar is the future, but getting there incrementally is the only realistic approach for most organizations. Capa-Java gives you a path to get from where you are today to where you want to be tomorrow, without having to rewrite everything overnight.

I've put hundreds of hours into this project over the years, and I'm still proud of it. It's not going to win any architecture awards for being the "coolest" approach, but it solves a real problem for a lot of people, and that's what matters.


Now I want to hear from you: What's your experience with Multi-Runtime and Dapr? Are you in an enterprise trying to adopt incrementally, or are you going all-in on Sidecar greenfield? Have you run into the same problem where big bang migrations just aren't feasible? Drop a comment below and let me know your thoughts — I'm genuinely curious to hear what other people are experiencing.

Check out the project on GitHub: https://github.com/capa-cloud/capa-java

Top comments (0)