This content originally appeared on DEV Community and was authored by Perry H
StokeBroker: My Journey Building an AI Surf Agent with Java and Embabel
Shots fired! Sorry Python people, but let me explain. Only a couple of days after I wrote CodeBro, my AI agent written in Java, I discovered this post called You Can Build Better AI Agents in Java Than Python. Ha! I love it (hence the clickbait title).
The post was written by Rod Johnson (creator of Spring framework) and he introduced his framework for writing AI agents on the JVM. Granted he was using Kotlin, but since I’ve been a Java guy for most of my professional career, I think Java is the way I will roll. I’ll dive into Kotlin later. The framework is called Embabel. So I figured I’d give it a run and see how it stacks up to my custom creation. Spoiler Alert: it’s way better.
Anyhow, as always, I want to create something but this is just for fun so it will serve no real purpose and will probably have no practical use. Not everything has to be a startup idea.
One of my hobbies is surfing, so I decided to create an agent that gets my local surf conditions and tells me if it’s good enough to call in sick (Don’t tell). Mind you, you can get most of this info from Surfline, but like I said, I’m just messing around so chill.
I shall call it StokeBroker.
Embabel
First we have to figure out how Embabel works. They have some pretty nice docs but who has the time to read them? So naturally I jump straight to the quickstart.
Embabel docs recommend putting the token as an .env variable. This didn’t work at first… wonderful. I took a look at the startup script and it was not loading the .env file. Fixed that by adding the proper configuration to the script.
Boom! That worked and I already see the demo running. It shows much more output than CodeBro, including detailed plan steps and even cost and token usage at the end. Slick.
Now I gotta figure out how to plan out StokeBroker. Crap, I may actually need to read the docs for this…
Unlike standard “chain” frameworks (like LangChain) that just string prompts together, Embabel uses GOAP (Goal-Oriented Action Planning).
Most AI agents work like a flowchart:
Step 1 -> Step 2 -> Step 3. (If Step 2 fails, the whole chain breaks).
Embabel works like a GPS:
Destination: “Find safe surf.” Route: “I see a traffic jam (Bad Wind), so I will reroute to a different beach (Action B) to get to the destination.”
It separates the “What” (Goal) from the “How” (Plan). I can see how this is useful.
Another interesting thing is that Embabel allows us to mix non-AI operations with AI operations seamlessly. This appeals to my inner Java developer who likes clean separation of concerns.
The Plan
Here’s how I broke down the architecture:
│
├─── Agent Brain (Reasoning Layer)
│ ├─ Swell Energy Calculator
│ ├─ Wind Quality Analyzer
│ ├─ Spot Matcher (Rule Engine)
│ ├─ Board Recommender
│ └─ Alert Threshold Evaluator
│
├─── Data Acquisition Layer
│ ├─ NOAA Buoy Service (Wave Data)
│ ├─ NOAA Tide Service (Tide Predictions)
│ ├─ NWS Marine Forecast Service
│ └─ Data Aggregator & Cache
│
├─── Knowledge Base (In-Memory)
│ ├─ Spot Repository (Oak Island Breaks)
│ ├─ Wind Direction Rules per Spot
│ ├─ Tide Preferences per Spot
│ └─ Swell Window Configurations
│
└─── User Interface Layer
├─ CLI Shell (Spring Shell)
└─ Report Generator
Basically, I think we need a reasoning layer; a data acquisition layer; a data layer or knowledge base (which for simplicity will be in memory); and finally a shell to interact. This is similar to CodeBro but with the additional complexity of getting external data and storing it.
Building the Beast (or trying to)
So, armed with my architecture diagram (which looks way more professional than the actual code), I started hacking away.
The cool thing about Embabel is that you define Actions. These are like the Lego blocks of your agent’s brain. You just slap an @Action annotation on a method, and suddenly your AI knows how to do that thing.
I decided to build a Pipeline (fancy word for “doing things in order”).
extractLocation: First, I need to know where we are going. I let the AI figure this out from my incoherent rambling (e.g., “How’s the pier?”).
fetchConditions: This is where the Java magic happens. I wrote a boring old service to fetch JSON from NOAA. No AI here, just cold, hard data.
analyzeQuality: Here’s the kicker. I feed that raw data back into the AI, along with my “Knowledge Base” (a list of rules about wind direction and swell angle). The AI acts like a snobby local surfer and judges the conditions.
generateReport: The Goal. The Grand Finale. The AI takes all that info and writes me a poem… or a report.
Here is a snippet of the agent. Look how clean it is! (Ignore the imports I hid).
@Agent(description = "Analyzes surf conditions using buoy and tide data")
public class StokeBrokerAgent {
// ... services injected here ...
@Action(description = "Fetch current surf conditions")
public SurfConditions fetchConditions(SurfLocation location) {
// Calls NOAA API... returns data
}
@Action(description = "Analyze surf quality")
public SurfQuality analyzeQuality(SurfConditions conditions, Ai ai) {
// Asks the LLM: "Is this good?"
return ai.withDefaultLlm().createObject(..., SurfQuality.class);
}
@AchievesGoal(description = "Generate final surf report")
@Action
public String generateReport(SurfLocation location, SurfConditions conditions, SurfQuality quality, Ai ai) {
// The final output!
return ai.withDefaultLlm().createObject("Write a funny report...", String.class);
}
}
The “Wait, it actually works?” Moment
I fired up the shell. Yes, Embabel comes with a built-in shell, because why write a UI when you can look like a hacker from the 90s?
I typed: x "How is the surf at Oak Island?"
And then… nothing happened. Just kidding. The logs went crazy. Colossus (the AI personality, you can choose from a couple in Embabel) started “thinking.”
It realized it needed a location. It extracted “Oak Island”. It realized it needed to get conditions. It called my Java service. It realized it needed to judge the waves. It ran the analysis.
And then, it spit out this masterpiece:
THE BRUTAL TRUTH: FLATTER THAN A PANCAKE ON A GRIDDLE
Alright folks, gather ’round for the saddest surf report you’ll read all week. We’ve got a big fat ZERO out there today. … BOARD RECOMMENDATION: Your couch. Maybe a boogie board for the bathtub. DO NOT CALL OFF WORK. In fact, pick up an extra shift.
I mean, it hurts, but it’s accurate (I checked Surfline, which kind of already does this…I already said this was pointless).
Under the Hood: Why Embabel Actually Rocks
Okay, let’s geek out for a second. Why is this better than just writing a Python script with openai.chat.completions.create?
1. Structured Data is King
In Python, you often get a blob of text back and hope your regex works (ok maybe this is an oversimplification). In Embabel, you define Java Records (or Kotlin Data Classes).
public record SurfQuality(double score, String boardRecommendation, String reasoning) {}
When I ask the AI to analyze the surf, I don’t ask for text. I ask for an Object:
return ai.withDefaultLlm().createObject(prompt, SurfQuality.class);
Embabel handles the prompt engineering to force the LLM to output JSON, parses it, validates it, and hands me back a strongly-typed Java object. If the LLM hallucinates a field, Embabel throws an exception before it breaks my downstream code. That is peace of mind.
2. GOAP (Goal-Oriented Action Planning)
This is the “GPS” feature I mentioned. I didn’t tell the agent: “First call A, then call B.”
I just said:
-
extractLocationtakes UserInput -> returnsSurfLocation -
fetchConditionstakesSurfLocation-> returnsSurfConditions -
analyzeQualitytakesSurfConditions-> returnsSurfQuality -
generateReporttakesSurfQuality-> Achieves Goal
The framework figured out the dependency graph. If I add a new step later (like checkCrowdLevels), I just drop in the @Action and the planner automatically wires it in. It’s like Dependency Injection for logic.
3. The “Brain” vs. The “Body” 
StokeBroker separates the LLM (Brain) from the Services (Body).
Body: NOAABuoyService knows how to make HTTP requests. It’s dumb, reliable, and testable.
Brain: StokeBrokerAgent knows how to interpret that data.
This means I can unit test my services without paying OpenAI 5 cents every time.
Pro Tips for the Java Gang
If you want to try this (and you should), here’s my advice:
Start with the Shell: Don’t try to build a REST API or a React frontend immediately. Use the embabel-agent-starter-shell. It gives you that Colossus> prompt out of the box, which is invaluable for debugging what the agent is actually thinking.
Think in Records: Before you write a single prompt, define your data model. What exactly do you want the AI to give you? Define those Records first. It forces you to be clear about your intent.
Keep Actions Small: Don’t write one giant “DoEverything” method. Break it down. One action to parse input, one to fetch data, one to make a decision. This makes the “Chain of Thought” visible and debuggable.
Don’t Fear the Java: Yes, it’s verbose compared to Python. But when your agent starts getting complex, you will thank the gods of static typing.
Conclusion
So, is Java better than Python for AI agents?
Look, I’m biased. But having type safety, real services, and a structured framework like Embabel feels… robust.
StokeBroker is alive. It’s rude, it’s honest, and it’s running on the JVM. It might not save the world, but at least I won’t drive 45 minutes to the beach just to look at a lake. You can check the code out here.
Now if you’ll excuse me, the report says it’s flat, so I have some refactoring to do.
This content originally appeared on DEV Community and was authored by Perry H
