About Java's Structured Concurrency, Elasticsearch Java client, DevEx and Developer Advocate's job
There’s been quite a lot happening recently, and I wasn’t sure in which order I should tackle the issues, so I decided to cover them all in a single post. If some aspects need clarification or further detail, please DM me, and I’ll try to sort it out somehow (perhaps in a subsequent entry).
Revamp of Structured Concurrency in Java 25
First, let’s start with Java 25 on the horizon. For many vendors (if not all), it’s going to be an LTS release. And Structured Concurrency serves as a great example of why we need preview features. One could say that three preview releases are enough and the feature should be standardized. However, the OpenJDK folks decided otherwise, and I’m very glad they did. I think I’ll skip boring you with all the details about how Structured Concurrency will be shaped in Java 25 in every nitty-gritty detail. IMHO, reading the JEP 505 and watching Nicolai’s video can help a lot here, in addition to my own talks, e.g. Butcher virtual threads like a pr0.
IMHO, it’s great that throwIfFailed
is now the default, and therefore not needed, as I found throwing exceptions useful every time I tested this new feature.
Then, sealing the StructuredTaskScope
and creating new ones with open()
, where you can provide auxiliary configuration, is simply great for a number of reasons:
- It’s not that inheritance is always bad—I think I’ve seen a number of cases where it actually made sense and was the best option (so my point from CONTEXTVS STVLTE, that it’s not wise to always replace inheritance with composition, still stands). However, providing configuration in the form of a subclass doesn’t seem like a good idea to me. When I wrote my custom implementation of
StructuredTaskScope
(for a different success criterion), it always felt awkward. Also, if the evolution of Stream Collectors and Gatherers taught us something, it’s that for configuration it’s probably better to use composition. - For me, try-with-resources—meant to automatically close resources—somehow works better with open. I don’t know, I might be biased, but it just… looks better than a constructor.
- Sealing
StructuredTaskScope
seems to be exactly what Sealed Classes (and Interfaces) in Java 17 were meant to be used for: easing maintainers’ lives and making sure the number of “useful custom inventions” stays limited. - Being able to (re)use a custom thread pool (because virtual threads might not be the optimal choice for some kinds of tasks) or to configure other aspects is also cool.
- Last but not least, it also proves the usefulness of the release train concept and preview features: preview features might be okay to try in production as long as they’re used in a microservice1, and you run it in an environment you fully control—but not if you can’t control which JDK version people use to run your artifacts.
So all in all, in Java 25, a block with Structured Concurrency—where a single method is responsible within its scope for handling concurrent subtasks in a way that’s clear to read and understand—may look like this:
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow(),
config -> config
.withName("myStructuredConcurrency")
.withTimeout(Duration.ofSeconds(5))
.withThreadFactory(sctf))) {
var subtask1 = scope.fork(() -> action(...));
var subtask2 = scope.fork(this::anotherAction);
scope.join();
combineResults(subtask1.get(), subtast2.get());
}
The pattern used for configuration emerged independently in another part of the Java ecosystem. Which brings us to the new Elasticsearch Java client ;-)
Elasticsearch 9 from Java’s perspective
After I started working for Elastic, the construction of the Java client was, let’s be honest, one of my pet peeves. It’s not that it wasn’t working—it was perfectly functional, flexible code. In many frameworks you wouldn’t even need to construct the client manually, because the framework would do it for you based on environment variables or similar configuration.
The thing is, sometimes your microservice is written properly and really is a micro one, not bloated with tons of dependencies. In such cases, you end up with many mandatory lines just to create the ES client in vanilla Java, passing effectively only two things: the URL and the API key. Not the optimal signal-to-noise ratio, if you ask me.
Thankfully, the people I work with are not only clever, but they also listen to fellow developer advocates and implement fixes based on the feedback they hear. That’s why Elasticsearch version 9.0 came equipped not only with many great internal features, but also with a new client version, which allows us to write this:
try (var esClient = ElasticsearchClient.of(c -> c
.host(System.getenv("ES_URL"))
.apiKey(System.getenv("ES_API_KEY")))) {
// use the esClient as you need
}
I’m curious what you think about this. IMHO this is a great change:
- The
ElasticsearchClient
can finally be constructed in just a few lines, where each line is actually meaningful—not just boilerplate string concatenation. - All the underlying machinery uses default implementations, unless you explicitly provide your own (for valid reasons).
- Last but not least, it’s also auto-closeable, so after the try block ends, the client is properly closed, resources are released, and your program exits cleanly—no need for
System.exit()
(which we really shouldn’t use anyway).
How to name it?
As the old wisdom says, there are two big problems in Computer Science:
- naming
- cache invalidation
- off-by-one errors.
default
methods), using subclasses, or even the classic builder pattern. However, as indicated in a tweet by Brian Goetz, we might not have a proper name for this particular pattern.
So if you have a proposal, perhaps you could reply to Mr. Goetz’s tweet?
What does it have to do with developer advocacy?
I think I should write a separate entry about being a developer advocate—what it’s like, what the pros and cons are, what we do and what we don’t do (especially since I’ve been asked this question on several occasions).
In short, I’d say: we don’t sell, and we don’t act as propaganda officers. What we do (among other things) is representing fellow developers from the wild world within the organisations we work for. We take the best there is to offer, and we present it using the best means we can. In other words, our job is not just to broadcast from the inside out, but also to act as a two-way relay: transmitting expectations, lessons learned, and so on, from the outside in—even if that is frequently overlooked.
In this very case, I was able to bring feedback “from the community” and express the need for changes internally. That’s exactly why you should follow Sharat Chandler’s advice to attend the Hallway Track at conferences (and annoy developer advocates—they might be more effective than filing issues in public trackers ;-) )
DevEx!
Oftentimes this refers to Developer Experience, sometimes abbreviated as DevEx. You see, even when you create a high-performing piece of technology—even a true gem of software engineering—if people don’t know how to use it, or if it makes them feel clumsy, your job isn’t done. The best piece of technology is one that does all the above and doesn’t require a PhD in reading manuals ;-)
Conclusion
Java is still kicking!
-
A microservice is a service that can be completely rewritten by your team in max. 2 weeks; if the rewrite from scratch takes longer, it’s not a micro service, just a service. Definition by me and my friends. ↩︎