Java is an object-oriented programming language that allows engineers to produce software for multiple platforms. Our resources in this Zone are designed to help engineers with Java program development, Java SDKs, compilers, interpreters, documentation generators, and other tools used to produce a complete application.
Jakarta NoSQL: Why JPA Is Not Enough for the AI Era
From printTriangularNumber to Duff’s Device: Mastering Java Switch Statements Old and New
The MovieManager project has been updated to use JDK 25 and the AOT cache from project Leyden. Project Leyden is part of the OpenJDK project and provides cached linking and cached performance statistics. That means the time spent linking at startup is moved to build time, and the statistics are created during a test run at build time as well. Because of that, the JVM loads the needed classes already linked and starts compiling the hot code paths immediately. The MovieManager application starts in less than half the time with these optimizations without any code changes. All these advantages come with preconditions: Exactly the same JVM version at build time, training time, and run timeThe same OS(Linux is used here) and libc at all steps -> (No Alpine-based Docker Images)Same CPU architecture, for example, AMD64 or ARM64 The steps to use Project Leyden: Build the Spring Boot ApplicationExtract the Spring Boot ApplicationDo a training run with the extracted Application to create the AOT cacheCreate the Docker Image with the extracted Application and the AOT cache Building and Training the Application The first step is to build the Spring Boot JAR. The MovieManager project has an integrated build that builds the Angular frontend and the Spring Boot backend with this Maven command: Shell ./mvnw clean install -Ddocker=true -Dnpm.test.script=test-chromium Project Leyden does not support Spring Boot Jars. The Jar has to be extracted to help Project Leyden find the used library jars of the project. To do that, this command needs to be used: Shell java -Djarmode=tools -jar backend/target/moviemanager-backend-0.0.1-SNAPSHOT.jar extract --destination extracted The result is the directory ‘extracted’ with the application jar and a sub-directory ‘lib’ that contains the used libraries. The second step is to create the AOT cache. To do that, the application has to run in production conditions. That means using a real PostgreSQL database with the database driver. That enables the JDK to record all the needed classes of the project and to create realistic performance statistics for the code compilation. To do this, a PostgreSQL database has to be started(done here in a Docker container), and the Application has to do the full startup. These commands are needed: Shell docker pull postgres:13 docker run --name local-postgres -e POSTGRES_PASSWORD=sven1 -e POSTGRES_USER=sven1 -e POSTGRES_DB=movies -p 5432:5432 -d postgres java -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+UseCompressedOops -XX:+UseCompactObjectHeaders -XX:+ExitOnOutOfMemoryError -XX:MaxDirectMemorySize=64m -XX:+UseStringDeduplication -Xlog:aot -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -Djava.security.egd=file:/dev/./urandom -jar extracted/moviemanager-backend-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod The Java command runs the application with the parameter ‘-Dspring.context.exit=onRefresh’ that makes Spring Boot do the full startup and exit then. The parameters ‘-Xlog:aot -XX:AOTCacheOutput=app.aot’ enable the logging of the AOT process and the creation of the ‘app.aot’ that is the AOT cache. The AOT cache contains everything that is needed for a fast startup of the application. If the AOT cache should also contain information to improve production performance, it would have to start up and process realistic production requests. That is beyond the scope of this article. The third step is to test the new application setup: Shell java -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+UseCompressedOops -XX:+UseCompactObjectHeaders -XX:+ExitOnOutOfMemoryError -XX:MaxDirectMemorySize=64m -XX:+UseStringDeduplication -Xlog:class+path=info -XX:AOTCache=app.aot -Xlog:aot -Djava.security.egd=file:/dev/./urandom -jar extracted/moviemanager-backend-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod The start-up time of the new setup with the AOT cache can be compared to the start-up time of the Spring Boot jar. On a medium-powered laptop, the times are: 9 seconds for the Spring Boot Jar3.5 seconds for the new setup with the AOT cache Creating a Docker Image To use the application in production, it needs to be packaged into a Docker image. The Docker image needs to contain the extracted application setup and the AOT cache. The base image needs to have the exact same JDK version, OS, and the same libc. That means small base images like Alpine cannot be used. The created Image can not be small because it contains 180 MB of AOT cache and a larger base image. This can be done with this Dockerfile: Dockerfile FROM eclipse-temurin:25.0.3_9-jdk-jammy WORKDIR /application ARG JAR_FILE=extracted/*.jar COPY ${JAR_FILE} moviemanager-backend-0.0.1-SNAPSHOT.jar COPY extracted/ ./ COPY app.aot app.aot ENV JAVA_OPTS="-XX:+UseG1GC \ -XX:MaxGCPauseMillis=50 \ -XX:+UseCompressedOops \ -XX:+UseCompactObjectHeaders \ -XX:+ExitOnOutOfMemoryError \ -XX:MaxDirectMemorySize=64m \ -XX:+UseStringDeduplication" ENTRYPOINT exec java $JAVA_OPTS -XX:+AOTClassLinking \ -XX:AOTCache=app.aot \ -Xlog:class+path=info \ -Djava.security.egd=file:/dev/./urandom \ -jar moviemanager-backend-0.0.1-SNAPSHOT.jar It copies the new application setup in the image and adds the AOT cache. The name of the application jar is in the AOT cache and has to be exactly the same as during the creation of the AOT cache. The ‘JAVA_OPTS’ also have to be the same. If the JDK version in the build environment changes, the version of the base image has to be adjusted accordingly. The parameter ‘-Xlog:class+path=info’ makes analyzing AOT problems much easier. The Docker container size is 705 MB. That makes the container about double the size of a Docker container with a Spring Boot Jar and an Alpine-based JDK image. Creating a Build Pipeline Creating Docker images for an application by hand is unsustainable in a production environment. A build pipeline is needed. The MovieManager project is hosted on GitHub; because of that, the project uses a GitHub Workflow as a build pipeline. The complete code for the build pipeline is in the script. The steps of the GitHub pipeline can be recreated in other environments too. The first step is to set up the PostgreSQL database service to be used in this build: YAML jobs: analyze: name: Analyze runs-on: ubuntu-latest env: POSTGRES_URL: jdbc:postgresql://localhost:5432/movies services: postgres: image: postgres:latest env: POSTGRES_USER: sven1 POSTGRES_PASSWORD: sven1 POSTGRES_DB: movies ports: - 5432:5432 options: >- --health-cmd="pg_isready -U sven1 -d movies" --health-interval=10s --health-timeout=5s --health-retries=5 The commands set up the PostgreSQL service in the build pipeline with user, password, dbname, and dbport. The ‘POSTGRES_URL’ is set to access the database later. The second step is to check out the project: YAML steps: - name: Checkout repository uses: actions/checkout@v3 It checks out the contents of the master branch. The third step is to provide the JDK: YAML - name: Setup Java JDK uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: 25 JDK version 25 is the minimum to use the project Leyden with linking and performance statistics. The fourth step builds the Spring Boot Jar: YAML - name: Build with Maven if: matrix.language == 'java' run: | ./mvnw clean install -Ddocker=true That is the Maven command to build the project. The fifth step is to find the Spring Boot jar: YAML - name: Find fat jar if: matrix.language == 'java' id: jar run: | JAR_PATH=$(find ./backend/target -type f -name "*SNAPSHOT.jar" | head -n 1) echo "Found JAR: $JAR_PATH" echo "jar=$JAR_PATH" >> $GITHUB_OUTPUT The sixth step is to extract the Spring Boot jar: YAML - name: Unpack fat jar if: matrix.language == 'java' id: UNPACK run: | java -Djarmode=tools -jar ${{ steps.jar.outputs.jar } extract --destination extracted EXTRACTED_PATH=$(find . -type d -name "extracted" | head -n 1) echo "Found directory: $EXTRACTED_PATH" echo "extracted=$EXTRACTED_PATH" >> $GITHUB_OUTPUT The seventh step is to get the name of the extracted application jar: YAML - name: find extracted jar if: matrix.language == 'java' id: EXTRACT run: | EXTRACTED_JAR=$(find "${{ steps.UNPACK.outputs.extracted }" -type f -name "*.jar" | head -n 1) EXTRACTED_JAR=${EXTRACTED_JAR#./} echo "Found extracted JAR: $EXTRACTED_JAR" echo "extracted=$EXTRACTED_JAR" >> $GITHUB_OUTPUT The eighth step is to create the AOT cache: YAML - name: Create AOT cache if: matrix.language == 'java' id: AOT env: JAVA_TOOL_OPTIONS: "" _JAVA_OPTIONS: "" JDK_JAVA_OPTIONS: "" run: | EXTRACTED_JAR="${{ steps.EXTRACT.outputs.extracted }" echo "jar=$EXTRACTED_JAR" echo "JAVA_TOOL_OPTIONS=$JAVA_TOOL_OPTIONS" echo "_JAVA_OPTIONS=$_JAVA_OPTIONS" echo "JDK_JAVA_OPTIONS=$JDK_JAVA_OPTIONS" JAVA_OPTS="-XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+UseCompressedOops -XX:+UseCompactObjectHeaders -XX:+ExitOnOutOfMemoryError -XX:MaxDirectMemorySize=64m -XX:+UseStringDeduplication" java $JAVA_OPTS \ -XX:+AOTClassLinking \ -XX:AOTCacheOutput=app.aot \ -Xlog:aot \ -Dspring.context.exit=onRefresh \ -Dspring.datasource.url="${{ env.POSTGRES_URL }" \ -Dspring.profiles.active=prod \ -jar "$EXTRACTED_JAR" || echo "AOT Training finished with exit code $?" This runs the application startup with the PostgreSQL database to create the AOT cache. The ninth step shows the exact JDK version used in the AOT cache generation: YAML - name: Show Jdk version if: matrix.language == 'java' id: JDK run: | JDK_VERSION=$(java -version 2>&1) VERSION=$(echo "$JDK_VERSION" | sed -n 's/.*build \([^[:space:]]*\)-LTS.*/\1/p') echo "JDK_VERSION=$JDK_VERSION" echo "VERSION=$VERSION" MY_VERSION="jdk=$VERSION" In case of problems with using the AOT cache. The first check is the version shown here against the JDK version in the Docker base image. The tenth step creates the Docker image: YAML - name: Build and push uses: docker/build-push-action@v6 if: matrix.language == 'java' with: context: . file: ./Dockerfile build-args: | JAR_PATH=${{ steps.EXTRACT.outputs.extracted } LIB_PATH=${{ steps.aot.outputs.extracted } push: false tags: angular2guy/moviemanager:latest This step can push the Docker image to an image repository. Conclusion The results of using the AOT cache of project Leyden are impressive. Cutting the startup time in half without any code change is amazing. The effort to create the AOT cache and set up the new application is a one-time investment. The impact of the larger Docker Images is low. That makes scaling application instances in Kubernetes clusters up and down much more flexible because the time to the availability of a new application instance is much lower. In Kubernetes environments with scaling of application instances, the AOT cache is a significant step forward and should be used. For serverless applications 3.5 seconds startup time is too slow. Their project, CrAC or Native Image, would be needed. Project CrAC needs code changes and testing. Native Image has the closed-world assumption, which makes it hard to prove that larger applications work correctly. Alternatives are Node.js with Nest.js and TypeScript, or Go with its libraries. Project Leyden is not finished in JDK 25. There are plans to add compiled code to the AOT cache in the future. The JVM is an impressive piece of technology that is still improving further.
Artificial intelligence is evolving beyond basic chat interfaces to play an active role in enterprise applications. While initial AI integrations often focus on text generation, summarization, or retrieval-augmented generation (RAG), many business challenges demand more advanced solutions. These require breaking down complex objectives into sequenced tasks and coordinating their execution. The Planning Pattern addresses this need by enabling AI to function as both a content generator and a strategist that creates execution plans. For software engineers and architects, the Planning Pattern marks a significant advancement in intelligent systems. It separates reasoning from execution, allowing applications to use large language models while ensuring governance, observability, and reliability in enterprise settings. This article demonstrates how to implement the Planning Pattern in Java, showing how an AI model can convert a high-level business goal into an actionable plan executed by deterministic application services. The resulting architecture blends AI creativity with the predictability and control needed for production systems. Project Setup and Dependencies To demonstrate the Planning Pattern, we will build a simple customer service application using Jakarta EE, CDI, and LangChain4j. The scenario is intentionally limited to highlight architectural concepts over business complexity. The application will serve as a customer support assistant, interpreting user requests and routing them to the correct workflow. For this article, we will implement only order cancellation. This approach keeps the AI layer independent from the business implementation. The assistant interprets customer intent and creates a plan, while application services remain deterministic and enforce business rules. This separation aligns with the Planning Pattern, which treats reasoning and execution as distinct responsibilities. The following dependencies form the foundation of our sample. Weld SE enables Jakarta CDI in standalone Java applications, SmallRye Config provides configuration support, and LangChain4j CDI integrates AI models into the Jakarta EE programming model. XML <dependencies> <dependency> <groupId>io.smallrye.config</groupId> <artifactId>smallrye-config-core</artifactId> <version>3.17.2</version> <scope>compile</scope> </dependency> <dependency> <groupId>io.smallrye.config</groupId> <artifactId>smallrye-config</artifactId> <version>3.17.2</version> </dependency> <dependency> <groupId>org.jboss.weld.se</groupId> <artifactId>weld-se-core</artifactId> <version>6.0.4.Final</version> </dependency> <dependency> <groupId>dev.langchain4j.cdi</groupId> <artifactId>langchain4j-cdi-portable-ext</artifactId> <version>${langchain4j-cdi.version}</version> </dependency> <dependency> <groupId>dev.langchain4j.cdi.mp</groupId> <artifactId>langchain4j-cdi-config</artifactId> <version>${langchain4j-cdi.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>1.15.0</version> </dependency> </dependencies> With the project configured, the next step is to create our first AI agent. With the project configured, the next step is to create the first AI agent. This agent will serve as the entry point for customer support, receiving natural-language requests and converting them into structured execution plans. Creating the AI Agent Contract The first component of our solution is the agent contract. In LangChain4j, an agent is represented as a simple Java interface, enabling developers to focus on business logic instead of framework details. This interface serves as the application's entry point to the AI model. In our customer support scenario, the agent's role is to receive customer requests and determine the appropriate resolution. Java public interface CustomerResolutionAgent { String resolveCustomer(String text); } While this interface appears simple, it represents a key architectural concept. Rather than embedding prompts, workflows, or AI-specific logic across the application, we define a business-oriented contract. LangChain4j dynamically generates the implementation, allowing the AI component to function like any other CDI-managed service. Implementing Enterprise Tools The Planning Pattern separates reasoning from execution. The model determines required actions, while business operations are implemented as deterministic services. These services are exposed as tools the AI can invoke when building and executing a plan. Java @ApplicationScoped public class EnterpriseTools { @Tool("Finds the internal customer id given a customer email address") public String getCustomerId(String email) { System.out.println("searching for email " + email); return "CUS-001"; } @Tool("Finds the order id given a customer id") public String getOrder(String customerId) { System.out.println("searching for customer " + customerId); return "ORD-001"; } @Tool("Cancels an order given its order id") public String cancelOrder(String orderId) { System.out.println("cancelling order " + orderId); return "cancelled"; } } Each method represents a business capability available to the agent. The @Tool annotation offers a natural language description to help the model determine when to use each operation. In production, these methods would interact with databases, external APIs, messaging systems, or domain services. For this example, we simulate the workflow by returning predefined values. The order cancellation process consists of several independent operations. The AI first identifies the customer, then locates the order, and finally executes the cancellation. This decomposition highlights the value of the Planning Pattern: the model determines the sequence of actions, while the application ensures each action is executed safely and predictably. Building and Running the Agent With the contract and tools defined, we can assemble the agent. The factory connects the language model, toolset, and interface contract into a single CDI-managed component. Java @ApplicationScoped public class ResolutionAgentFactory { @Inject private ChatModel chatModel; @Inject private EnterpriseTools tools; @Produces public CustomerResolutionAgent create() { return AiServices.builder(CustomerResolutionAgent.class) .chatModel(chatModel) .tools(tools) .build(); } } Conclusion The Planning Pattern represents an important architectural evolution in enterprise AI systems. Rather than treating a language model as a simple text generator, it elevates AI to the role of strategist, capable of decomposing business objectives into executable plans while leaving execution to deterministic application services. By separating reasoning from execution, architects gain the flexibility of AI-driven decision-making without sacrificing governance, observability, or reliability. The language model determines what should happen, while enterprise services remain responsible for how those actions are performed. This distinction preserves existing business rules, security controls, and integration boundaries while enabling more adaptive user experiences. In this article, we implemented a customer support assistant using Jakarta EE, CDI, and LangChain4j. The agent interpreted a high-level customer request, identified the required sequence of operations, and coordinated enterprise tools to complete the workflow. Although the example focused on order cancellation, the same architecture can support a wide range of enterprise scenarios, including customer onboarding, account management, claims processing, inventory management, and operational workflows. As organizations move beyond chatbots and retrieval-based applications, patterns such as Planning become increasingly valuable. They provide a structured approach for integrating AI into business processes while maintaining the predictability and control expected from enterprise software. The result is an architecture where AI contributes reasoning and adaptability, while deterministic services continue to provide the reliability required for production environments.
If you've ever written raw JDBC, you know what's coming. Open a connection, create a PreparedStatement, set parameters by index (hope you counted right), iterate a ResultSet, close everything in a finally block, declare SQLException on every method signature… It's a lot of ceremony for "give me some rows." I've been experimenting with Ujorm3, a new lightweight ORM library for Java 17+. Here's a realistic example — a JOIN query that maps results including a nested relation: Java static final ResultSetMapper<Employee> EMPLOYEE_MAPPER = ResultSetMapper.of(Employee.class); List<Employee> findEmployees(Connection connection, Long minId) { return SqlQuery.run(connection, query -> query .sql(""" SELECT e.id, e.name, c.name AS "city.name" FROM employee e JOIN city c ON c.id = e.city_id WHERE e.id >= :minId """) .bind("minId", minId) .toStream(EMPLOYEE_MAPPER.mapper()) .toList()); } Let me walk through what makes this tick. Fluent API The whole operation is one readable chain. No juggling Statement objects, no passing things between methods — you declare the SQL, bind parameters, specify the mapper, and collect. Done. Named Parameters Instead of Positional ? Classic JDBC: Java stmt.setLong(1, minId); // hope you counted correctly Ujorm3: Java .bind("minId", minId) You reference parameters by name in the SQL (:minId) and bind them by name. No counting, no off-by-one errors when you insert a new parameter in the middle of a query, and the SQL stays readable. No Checked Exceptions SQLException is a checked exception, so vanilla JDBC forces you to handle or rethrow it everywhere — even when there's nothing useful to say. Ujorm3 wraps these internally, so your methods stay clean: Java // JDBC — forced to declare or catch List<Employee> findEmployees(Connection c, Long minId) throws SQLException { ... } // Ujorm3 — nothing to declare List<Employee> findEmployees(Connection connection, Long minId) { ... } Smart Object Mapping — Including Relations ResultSetMapper is a thread-safe class that prepares its mapping model on first use and reuses it across all subsequent calls. This significantly reduces overhead when processing a large number of queries. Mapping is inferred automatically by default. You can optionally annotate your domain classes with standard jakarta.persistence annotations (@Table, @Column, @Id) for explicit control, but they're not required. The interesting bit is how it handles relations. The aliased column "city.name" uses dot notation to map directly into a nested object — no extra configuration needed: SQL -- maps to employee.getCity().getName() automatically c.name AS "city.name" The library supports M:1 relations. 1:M collections are intentionally left out — a deliberate design choice to avoid hidden queries and N+1 problems. Want Compile-Time Safety? There's a Metamodel for That The string-based alias approach works great for getting started, but if you want the compiler to catch typos in column mappings, the optional APT plugin generates Meta* classes from your domain objects. The query then looks like this: Java List<Employee> findEmployees(Connection connection, Long minId) { return SqlQuery.run(connection, query -> query .sql(""" SELECT e.id AS ${e.id} , e.name AS ${e.name} , c.name AS ${c.name} FROM employee e JOIN city c ON c.id = e.city_id WHERE e.id >= :id """) .label("e.id", MetaEmployee.id) .label("e.name", MetaEmployee.name) .label("c.name", MetaEmployee.city, MetaCity.name) .bind("id", minId) .toStream(EMPLOYEE_MAPPER.mapper()) .toList()); } The ${placeholder} syntax in the SQL template and the label() method work together — the metamodel keys are type-parameterized descriptors that resolve column labels at runtime and carry full type information. Automatic Resource Management SqlQuery.run(...) handles closing the underlying PreparedStatement and ResultSet for you. No try-with-resources, no resource leaks if mapping throws partway through. There's More Than Just SqlQuery The library offers three levels of abstraction — pick what fits your use case: EntityManager – the fastest path for CRUD on a single table using a primary key; generates the SQL itself.SelectQuery – for fetching data including relations; supports type-safe Criterion filters composable with AND/OR operators; JOIN type (INNER vs LEFT) is inferred automatically from the nullable property of @Column.SqlQuery – low-level, full native SQL control; what we've been looking at above. SelectQuery in Action In many cases, the full SELECT statement — columns, JOINs, and WHERE clause — can be generated automatically by SelectQuery from the metamodel, so you don't have to write SQL at all. You still get the same object mapping under the hood. First, set up the shared context and entity manager (once, typically as static fields): Java // EntityContext controls SQL logging; false = no param values in logs static final EntityContext CTX = EntityContext.ofSqlInfoWithParams(false); static final EntityManager<Employee, Long> EMPLOYEE_EM = CTX.entityManager(Employee.class); Then the query itself: Java List<Employee> findEmployees(Connection connection, Long minId) { return SelectQuery.run(connection, EMPLOYEE_EM, query -> query .columns(true) // select all columns, including foreign keys .column(MetaEmployee.city, MetaCity.name) // add the city.name JOIN column .where(MetaEmployee.id.whereGe(minId)) // WHERE id >= minId .tail("ORDER BY", MetaEmployee.id) // append raw SQL fragment at the end .toList() ); } A few things worth noting: .columns(true) expands to all mapped columns of Employee, including foreign key values (e.g. city_id). The true argument does not affect JOIN generation yet — that is driven by the next call..column(MetaEmployee.city, MetaCity.name) adds a specific column from a related entity. The library resolves which JOIN to emit based on the metamodel..where(...) takes a type-safe Criterion. Conditions compose naturally with .and() / .or(), and because they're built from metamodel descriptors, a typo in an attribute name is a compile error, not a runtime surprise..tail("ORDER BY", MetaEmployee.id) appends a raw SQL fragment after the generated WHERE clause — a handy escape hatch for ORDER BY, LIMIT, window hints, or anything else the query builder doesn't cover. The result mapping works exactly the same way as in the SqlQuery examples above — same ResultSetMapper machinery, same dot-notation for nested objects. Performance Instead of reflection, the library generates and compiles its own bytecode at runtime for reading and writing domain object fields — performance comparable to handwritten code. In benchmark comparisons against Hibernate, Jdbi, MyBatis, and others (running on PostgreSQL and H2) it performs very well. The entire compiled module, including Ujorm3 itself, is under 3 MB, which is nice for microservices. What This Is NOT Not Hibernate. No entity scanning, no session factory, no proxy objects, no lazy loading surprises. You write SQL, you get objects back. Not jOOQ either — there's no Java DSL for building queries. You write plain SQL strings, which means you get full access to any database-specific syntax: window functions, CTEs, vendor extensions, whatever your DB supports. Getting Started Java 17+, final version 3.0.0 available on Maven Central: XML <dependency> <groupId>org.ujorm</groupId> <artifactId>ujo-core</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>org.ujorm</groupId> <artifactId>ujorm-orm</artifactId> <version>3.0.3</version> </dependency> Optional APT plugin for metamodel generation: XML <annotationProcessorPaths> <path> <groupId>org.ujorm</groupId> <artifactId>ujorm-meta-processor</artifactId> <version>3.0.3</version> </path> </annotationProcessorPaths> Integration tests cover PostgreSQL, MySQL, MariaDB, Oracle, and MS SQL Server (all via Docker). When Does This Make Sense? If you need JPA portability across databases or your company mandates a standard ORM, use Hibernate. If you want full SQL control, transparent behavior, and no hidden magic — and you'd rather not write raw JDBC — this hits a nice sweet spot. Useful links: Project homepagePetStore demoBenchmark testsJavaDocMore examples as JUnit tests Curious whether others are using similar lightweight wrappers, or if you've landed on a different approach for native SQL without going full ORM.
Apache Spark is one of the most powerful tools in the data and AI engineering world. It helps process massive datasets and is widely used across industries, irrespective of cloud platforms. But when you move from learning Spark to running it in production, you start seeing real challenges. This is from practical experience. 1. JVM Overhead Spark runs on the Java Virtual Machine (JVM). At first, this looks fine. But in real workloads, it creates overhead. What actually happens: Extra memory is consumed by the JVM itselfData moves between Python and JVM (serialization)Job startup takes more time Why it matters: Even if your logic is simple, the JVM layer adds hidden cost and latency. Especially in PySpark workloads, this becomes very noticeable. 2. Garbage Collection (GC) Issues The JVM uses garbage collection (GC) to manage memory. In small workloads, no problem. In large workloads, big problem. What we generally observe: Sudden pauses during execution, Jobs becoming slow without a clear reason, and performance behaving inconsistently. Real Challenge We often need to tune: memory settings, GC configuration, and executor behavior. Without proper tuning, performance becomes unpredictable. 3. Cluster Complexity Spark is not just a tool — it is a distributed system. To run it, you must manage infrastructure. What we need to handle: Cluster setup, executors and memory configuration, partition tuning, scaling (up/down). Impact in real projects: Higher infrastructure cost, more operational effort, requires deep expertise, and this adds overhead beyond just writing data pipelines. Rust Changes Everything Rust solves these problems at the language level. No JVM Rust compiles directly to machine code. So, no virtual machine and no runtime overhead. No Garbage Collection Rust uses ownership-based memory management. Memory is handled at compile time No runtime GC pauses Predictable Performance Better memory control, no hidden pauses, Efficient execution Result: Faster and more stable systems When we look at Rust tools, we see different ways: Replace Parts of Spark PolarsDataFrame processingDataFusionSQL engineBallistaDistributed executionRisingWaveStreamingSailFullSpark replacement Lakesail has came up with all together at once place. What Is Sail? Sail is an open-source computation framework that serves as a drop-in replacement for Apache Spark (SQL and DataFrame API) in both single-host and distributed settings. Built in Rust, Sail runs ~4x faster than Spark while reducing hardware costs by 94%. In simple terms: Sail = Spark experience + Rust performance + no JVM/GC problems It is not just a library. It is a full data platform / compute engine. Core Idea of Sail Traditional Spark: Plain Text PySpark → JVM → Spark Engine → Execution Sail: Plain Text PySpark → Spark Connect → Sail (Rust Engine) → Execution Key difference: Spark depends on JVMSail removes the JVM completely Where Sail Is Strong Sail is a good choice if you are already using Apache Spark and want better performance.It allows you to continue using the same Spark SQL and DataFrame APIs without rewriting your code.It removes JVM and garbage collection overhead, which helps improve speed and memory usage.Because it runs on a Rust-native engine, it provides more stable and predictable performance.It can help reduce infrastructure cost while keeping your existing development approach. Where You Should Be Careful Sail is still a new technology and not as mature as the Spark ecosystem.The number of connectors, integrations, and community support is smaller compared to Spark.Some advanced Spark features may not be fully supported yet.It is important to test Sail with your own workload before using it in production. Sail supports almost all modern platforms' emerging features: Local mode (single machine)Cluster mode (Kubernetes) It includes: Task schedulingResource managementDistributed execution Similar to a Spark cluster, but lighter Lakehouse Support Sail supports: Delta LakeApache Iceberg That means: Works with modern data lakesCompatible with existing data Storage Support Sail can read/write from: AWS S3Azure Data LakeGoogle Cloud StorageHDFSLocal files So, it integrates with existing ecosystems Catalog Integration Supports: Unity CatalogIceberg REST Catalog Important for: GovernanceAccess controlEnterprise data management Multimodal + AI Workloads Sail goes beyond Spark. It supports: Structured dataImagesPDFsAI workloads This is called: Multimodal lakehouse. Performance and Cost Sail claims: ~4x faster executionUp to 8x in some workloads~94% lower cost Reasons: No JVM overheadNo GCBetter memory usage Conclusion Sail is a new way to run Spark workloads using Rust instead of the JVM. It removes garbage collection and reduces memory and performance issues, making execution faster and more stable. One of its biggest advantages is that you can keep the same Spark code with little or no changes. This helps reduce infrastructure cost and complexity. However, it is still a new technology and not as mature as Spark yet. In the future, the best approach will be to use the right mix of Spark and Rust tools together.
Last week was about Metal and the Skin Designer. This week, the headline items are about what a brand new project looks like when you generate it: the default JDK is Java 17, and every generated project ships with an AGENTS.md authoring skill that lets any modern AI agent work on the project intelligently. There are also some other things worth covering: a runtime accent palette on the new native themes, three Metal follow-ups (one of which introduces a new matrix-correct translate API), the JDK 11+ String API gap closed, and iOS push permission that no longer fires at app launch. What is Codename One? Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at codenameone.com. Java 17 by Default We changed the default projects generated by the Initializr to Java 17+ to focus on the future of Codename One. The existing Java 8 option in the Initializr is still selectable from the radio panel if you have a reason to use it. Pick whichever you want. The Java 17 path is the one we now recommend for new work. Generated projects build with any JDK from 17 onwards (we routinely test on 21 and 25); you do not need to install Java 17 specifically. The bigger picture of how Java 17 support works in the toolchain, including which language features land in your app code and how the iOS / Android ports handle the newer bytecode, was covered in Official Experimental Java 17 Support earlier this year. The change this week is the default and the wording: the (Experimental) tag is gone, and Java 17 is now what you get unless you opt out. AGENTS.md and the Codename One Skill The other change in PR #4946 is that every Java 17 project the Initializr generates now ships an AGENTS.md file at the project root and a Codename One authoring skill alongside it. AGENTS.md is the convention for handing project-specific context to any AI agent. Claude Code, Cursor, Codex, Aider; they all look for it. Codename One projects now ship one. The actual skill content lives under .agent-skills/codename-one/ (vendor-neutral) and the source for it is in the repo at scripts/initializr/common/src/main/resources/skill if you want to read through it directly. There is also a thin stub at .claude/skills/codename-one/SKILL.md so Claude Code's /skills picker indexes it; the stub redirects to the same vendor-neutral content. We deliberately scoped this to Java 17 projects. The older Java 8 build had additional constraints (Java 5/8 source target, retrolambda, the historical bytecode rewrite rules) that made the "what can I actually use" answer noticeably more complicated. Restricting the skill to Java 17 lets us give agents a cleaner picture of the language level, the toolchain, and the build commands without spending half the SKILL.md on caveats. If you stay on Java 8, you keep the project layout you had; nothing changes for you. A few things the skill makes possible that I think are genuinely useful: Agents can debug a Codename One app under jdb. This is the one I am most pleased with. The simulator is a regular JVM, so the standard Java Debugger attaches cleanly, but agents previously had no idea this workflow was available. The skill's debugging.md reference walks through starting the simulator with the right -Xrunjdwp flags, attaching jdb, setting breakpoints, dumping locals, and stepping. The same workflow works in CI and any headless context where a graphical debugger is not an option. For an LLM that is otherwise reduced to "add a println and hope", this is a much sharper tool. Agents can check whether an API is part of the Codename One subset before they suggest it. Codename One targets a Java 5/8 shaped JDK, so the same bytecode translates to iOS, Android, and JavaScript. An agent that has only read regular Java idioms will routinely reach for java.nio.file, java.time, or pieces of java.util.concurrent that the framework does not include. The skill ships a single-file IsApiSupported.java tool that an agent can invoke to verify a class or method before writing code against it. Agents can validate a CSS snippet before applying it. Codename One CSS is its own subset; rules that look fine to a browser developer get silently dropped by the compiler. The IsCssValid.java tool lets the agent confirm the compiler will accept a snippet without booting the simulator. These three things together are most of why an agent that was previously polite-but-not-useful on a Codename One project is now actually productive on one. If you do not use agents, the same Markdown is one of the better tours of the framework's mental model that we have written; open .agent-skills/codename-one/SKILL.md in any project you generate today and read top to bottom. Native Theme Accents PR #4884 closes the loop on the new iOS Modern and Material 3 native themes we shipped two weeks ago. The native themes now expose their accent palette as named theme constants, so rebranding your app to your own colors is a five-line CSS change instead of a fork. Override the constants inside the #Constants block of your own theme.css: CSS #Constants { includeNativeBool: true; darkModeBool: true; --accent-color: #ff2d95; --accent-color-dark: #ff2d95; --accent-pressed-color: #c71a75; --accent-on-color: #ffffff; That is it. Every accent-bearing UIID picks up the new color. Light and dark are independent (--accent-color vs --accent-color-dark), and partial overrides are fine; anything you do not redeclare stays at the framework default. Material 3 has a couple of additional container-tier constants for the elevated-surface tone; iOS ignores those. There is also a runtime path for dynamic theming (in-app accent toggles, branded flavors, A/B tests). It uses the same constants. The Native Themes chapter of the developer guide covers it in detail, along with the full iOS and Android constant tables and the places where the binding system intentionally does not apply: Accent palette override. The point worth pulling out: the parts of theming that do not change per app (which UIIDs participate in the accent palette, which states they expose, which dark-mode counterparts they have) live inside the framework and stay there. The parts that do change per app (your colors) live in your project as five constants and nothing else. That is the whole reason this change exists. Metal Follow-Ups Last week was about shipping the Metal renderer. This week is the follow-up week: three PRs, plus one new API on Graphics that I think will quietly pay for itself many times over. Per-Axis Scale Decomposition (#4939, fixes #3302) Long-standing issue #3302 had a clear repro: g.translate + g.scale(sx, sy) + fillShape with sx != sy produced shapes that visibly drifted off the axis-aligned drawRect and drawLine calls the framework emitted alongside them. Triangles inscribed in rectangles escaped their bounding rect. The cause was that the legacy alpha-mask path rasterized the shape at a uniform scale (the diagonal ratio h2/h1), then stretched the resulting texture non-uniformly through the GPU matrix to recover the requested aspect. The bbox math is exact in real numbers, but the texture is pixel-rounded at the intermediate uniform scale, so the stretch drifted the rasterized shape off the pixel grid that drawRect and drawLine were already on. The fix factors the user transform's 2x2 linear part by taking the column norms as (sx, sy), rasterizes the path at S(sx, sy) so the per-axis stretch happens at rasterization time against a vector path rather than a pixel grid, and applies only the residual transform * S(1/sx, 1/sy) on the GPU. The residual is pure rotation (and shear in the worst case), so no per-axis stretch happens at sample time, and the alpha-mask texture lands on the same pixel grid as its drawRect siblings. The change is gated to Metal; the GL ES2 path keeps its legacy branch, so the existing GL goldens are byte-identical. A new InscribedTriangleGrid screenshot test was registered with Cn1ssDeviceRunner so the inscribed-triangle property is now visually verifiable in CI. Clip-Under-Rotation Diagnostic (#4924, towards #3921) PR #4924 does not fix a bug, it localizes one. Issue #3921 is "clip-under-rotation behaves wrong on some ports", entangled with a getClip / setClip(int[]) round-trip limitation the reporter himself called out as a separate issue. To split the two, we shipped a screenshot test that uses only pushClip / popClip and rotateRadians. The clip becomes non-axis-aligned via clipRect inside a 30-degree rotation, which forces the framework through its polygon-clip branch. The expected outcome is a 30-degree-tilted red fill that overlaps the navy outline at two diagonal corners and falls short at the other two. Two distinguishable failure modes are pre-labeled in the PR: the clip widened to its axis-aligned bbox (red exactly matches the navy outline), or the polygon clip dropped entirely (red fills the whole cell). When the iOS Metal cell of this test renders, we know within a glance which of the three behaviors we are looking at. The expected-failure cell is also a hypothesis: ClipRect.m's polygon initialiser stores x = y = w = h = -1, and the Metal execute path then calls CN1MetalSetScissor(0, 0, -2, -2), whose width <= 0 / height <= 0 branch sets the scissor to the full framebuffer instead of the intended polygon. If the screenshot confirms the hypothesis, the fix is a one-line replacement of the polygon-scissor fallback. iOS Metal Color Space Hint (#4909, fixes #4908) PR #4909 adds an ios.metal.colorSpace build hint. Until this week, the Metal layer's CAMetalLayer.colorspace was hard-coded to sRGB. For most apps, that is right; sRGB is what your existing assets are authored in. But on iPhone XR and later, Apple's screens are wide-gamut (Display P3), and a marketing-led brand that ships P3 artwork was visibly losing saturation by being routed through the sRGB pipeline. Accepted values are sRGB (default), displayP3, deviceRGB, linearSRGB, extendedSRGB, extendedLinearSRGB, and none. Set it in codenameone_settings.properties: Java codename1.arg.ios.metal.colorSpace=displayP3 The hint is dormant when ios.metal=false, so existing GL builds are unchanged. Unrecognized values produce a warning log and fall back to sRGB. Documented under Working-With-iOS.asciidoc. The New translateMatrix API The Inscribed-Triangle-Grid test in #4939 also surfaced a quiet papercut in Graphics that is worth pulling out as its own feature. Graphics.translate(int, int) does not compose into the affine transform the way scale() and rotateRadians() do. It accumulates into a per-Graphics integer offset that is added to draw coordinates before the impl matrix is applied. That is a holdover from the very first version of the framework, when Graphics did not have a matrix at all. Today the consequence is surprising: a subsequent g.scale(sx, sy) multiplies the integer translate too, which means the same code produces visibly different positions depending on whether you scale before or after you translate. The new Graphics.translateMatrix(float, float) composes the translation directly onto the impl matrix, in the same way scale and rotateRadians already do. The result is uniform "post-multiply translate onto the current transform" semantics across iOS (both GL and Metal), JavaSE, Android, and the JavaScript port. Same code, same on-screen position, whether you are drawing into a Form's Graphics or a mutable Image's Graphics. Java // Matrix-correct composition. Use this when you want translate to // behave like scale and rotate (composed into the affine transform). g.translateMatrix(centerX, centerY); g.rotateRadians(angle); g.scale(sx, sy); g.translateMatrix(-centerX, -centerY); For app code writing affine-transform pipelines (the "translate to pivot, rotate, scale, translate back" idiom from Java2D and AWT), this is the API you want. isTranslateMatrixSupported() returns true on every modern port. The old translate(int, int) is not deprecated and is not going anywhere; half the framework's internal scrolling code is built on it. The new method is the one to reach for in new drawing code, particularly anything that combines translate with scale or rotate. String API: replace(CharSequence, CharSequence), replaceAll, replaceFirst PR #4893 closes a long-standing gap reported in issue #4878. The JDK 1.5+ overload of String.replace that takes CharSequence arguments (the one nearly every modern Java tutorial reaches for) was missing from the Codename One subset. So were String.replaceAll(String, String) and String.replaceFirst(String, String). Because none of the three were on the bootclasspath, code that reached for them did not compile against a Codename One project at all; you had to know to fall back to the older replace(char, char) overload and to roll your own regex. All three are now wired in. String.replace(CharSequence, CharSequence) has a real implementation in vm/JavaAPI. replaceAll and replaceFirst are wired through the bytecode-compliance rewriter to a new JdkApiRewriteHelper pair that delegates to the existing RE regex engine (the same pattern we have been using for years on String.split). New compliance tests cover both rewrite rules. It is a small change in line count. In practice, it is a noticeable reduction in how often "I copied a snippet from Stack Overflow, and it didn't work on iOS" turns into a real bug. Three of the most-reached-for String methods in modern Java are now part of the on-device API. iOS Push Permission No Longer Fires at App Launch PR #4894 fixes issue #4876. With ios.includePush=true the framework used to call requestAuthorizationWithOptions from application:didFinishLaunchingWithOptions:, which meant the iOS system permission dialog fired as soon as the app finished launching, before the user had seen any of your screens. There is no good way to recover from a "Don't Allow" tap at that point. The user has not experienced the app yet, does not know why notifications matter, and tapping Don't Allow is the path of least resistance. Once denied, re-prompting requires sending the user out to Settings. The fix moves the prompt to the natural points. Push.register() triggers the system prompt (this code path already requested permission inside IOSNative.m; we just stopped firing it ahead of time). LocalNotification.schedule() also triggers it, via a new requestAuthorizationWithOptions call in sendLocalNotification. Same flow Android has been on for years. The practical consequence is that you can now show your own rationale screen ("we'd like to ping you when your order ships") before the system dialog fires. If you have an app that needs the legacy launch-time behavior, a backward-compatibility build hint restores it: Java codename1.arg.ios.notificationPermissionAtLaunch=true The default is false, so existing apps that did not opt in pick up the new behavior on next rebuild. Documented in Push-Notifications.asciidoc. The cloud-side build server change shipped as BuildDaemon #71, so local and cloud builds match. One thing to flag if you are updating an existing iOS app: if your onboarding flow was relying on the launch-time prompt happening automatically, your prompt now never fires unless Push.register() or LocalNotification.schedule() is invoked somewhere. That is almost certainly what you want, but check that the call lands. Skin Designer FAQ Follow-Up A few questions came up on discussion #4928 after last week's Skin Designer post, worth pulling forward here because they keep coming up in the same shape: Skins do not affect CSS. The skin is simulator scaffolding (device frame, screen rect, cutouts, safe-area insets); your theme.css and your native theme are unrelated.For a known device, the defaults are usually right. Pick the device, hit Pick a shape, click Finish. The customization UI is there for when our device database is incomplete (the iPhone 17e entry might say "no notch" when it actually has one, or the notch position might be off by a few pixels); when you have a physical device to measure against, that is where you refine.Themes are leaving skins. Historically, the native theme was bundled inside each skin because that is what made sense at the time. Going forward, the right home for them is the framework itself, distributed via Maven, so you pick up updates automatically. The new native themes already work this way. The per-skin embedded theme stays for legacy compatibility, and the Skin Designer still writes one for you, but the Native Theme menu we shipped two weeks ago is the path forward. The device database the Skin Designer reads from is open at scripts/skindesigner/common/src/main/resources/devices.json if you want to file a PR with a device we are missing or a row whose details are off. Wrapping Up Two reminders. First, flip ios.metal=true on your real app this week if you have not. The default flip is days away, and we would rather find any remaining edge case against your screens than against the install base on launch day. Second, if you have not generated a project from the Initializr recently, do it; the Java 17 default and the AGENTS.md skill are both worth seeing for yourself. A specific thank-you this week to the reporter on #3302 for sticking with the inscribed-triangle bug for as long as GL was the only target, Durank for the iOS push permission report on #4876, and the reporter on #4878 who flagged the missing String.replace(CharSequence, CharSequence); that one had been sitting in the gap for a long time. The issue tracker is here, the Playground and Initializr are the easiest places to poke at the new defaults, and the Skin Designer from last week is still there if you have a device shape you need a skin for.
It has been one of those weeks where the diff is bigger than the headline. The headline is short — Codename One now ships modern native themes: an iOS "liquid glass" look and an Android Material 3 look, bundled into the iOS and Android ports, on by default in the Playground, and selectable from a brand new menu in the simulator. The diff behind that headline is several thousand lines across the platform ports, the simulator, the GUI plumbing, and a small army of screenshot tests. What is Codename One? Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at codenameone.com. The theme behind the work is simple: Codename One should look modern out of the box on every platform we ship to, and it should feel fast. Almost everything in the past week of commits is in service of one of those two goals. Try It Right Now in the Playground The easiest way to see any of this is the Playground. The Playground now defaults to iOS Modern when the device toggle is set to iPhone and Android Material 3 when it is set to Android, in both light and dark mode. No setup, no pom.xml, no build hints — just open the page, drop in any of the standard components, and the modern look is what you get. If the past releases of Codename One looked dated to you, the Playground is where to start. The simulator is the second-easiest place. We will get to that. The New Native Themes For most of Codename One's life, the iOS native theme has been the venerable iOS 7 flat theme, and the Android native theme has been Holo Light. Both still ship — backward compatibility has always been one of our most important goals — but they are no longer where we want a brand new app to start. We spent the bulk of this week building two new themes that target current platform aesthetics: iOS Modern – Apple system colors (accent #007aff light / #0a84ff dark, grouped-form surfaces, the system separator palette), pill borders for tabs, an iOS-Settings-style MultiButton, CHECK_CIRCLE-style checkbox glyphs, and translucent surfaces for Dialog and TabsContainer so they read as glass-frosted on top of whatever is behind them. It is not a real UIVisualEffectView backdrop — that is a port-side primitive we have not built yet — but the look is much closer to the iOS 26 vibe than anything we have shipped before.Android Material 3 – the Material 3 baseline tonal palette (primary #6750a4 light / #d0bcff dark, surface-container tiers, elevated containers approximated tonally because real elevation drop-shadows are still on the to-do list), plus all the Material density and padding choices — Roboto-ish proportions, a top-tab bar with the underline-by-color treatment, the standard square checkbox glyph. Each theme covers the usual ~25 UIIDs: base (Component, Form, ContentPane, Container), typography (Label, SecondaryLabel, TertiaryLabel, SpanLabel*), buttons (Button, RaisedButton, FlatButton with .pressed and .disabled), text input, selection controls, toolbar, tabs, side menu, list, MultiButton, dialog/sheet, FAB, and all the supporting separator and popup pieces. Both themes have full light and dark coverage. The shipping CSS sources sit in the repo at native-themes/ios-modern/theme.css and native-themes/android-material/theme.css for anyone who wants to read what each UIID is doing. iOS Modern This is the ShowcaseTheme capture from the new screenshot suite, run on iOS in light and dark. Same Form, same components, swap Display.setDarkMode(...) and re-resolve. The form is built like this: Java Container row = new Container(BoxLayout.x()); row.add(new Button("Default")); Button raised = new Button("Raised"); raised.setUIID("RaisedButton"); row.add(raised); form.add(row); TextField tf = new TextField("[email protected]"); form.add(tf); Container toggles = new Container(BoxLayout.x()); CheckBox cb = new CheckBox("Remember me"); cb.setSelected(true); toggles.add(cb); RadioButton rb = new RadioButton("Agree"); rb.setSelected(true); toggles.add(rb); form.add(toggles); SpanLabel body = new SpanLabel("Body copy …"); That gives you the full picture on one screen: The Default button uses the stock Button UIID. The Raised button uses RaisedButton, which cn1-derives from Button and adds a tinted pill on top of the iOS system blue — that is the iOS Modern accent in both modes.The TextField is a single rounded-rect surface with the iOS system gray fill, the same shape Apple uses in Settings.CheckBox and RadioButton use the new optional @checkBoxCheckedIconInt / @radioCheckedIconInt theme constants to swap to CHECK_CIRCLE / CHECK_CIRCLE_OUTLINE glyphs — Reminders-app aesthetic on iOS, while Android keeps the standard square check.The SpanLabel body uses the theme's base font and inherits transparent backgrounds, so it never paints over a translucent parent. The full-screen source is DarkLightShowcaseThemeScreenshotTest.java. Android Material 3 Same ShowcaseTheme source on Android. The Material 3 baseline palette gives Default the primary container color and Raised the elevated-surface tone, with the dark variant flipping the relationship correctly via the dark color-role mapping. Padding and font sizing follow Material density, which you can see in how compact the same Form lays out compared to iOS. Translucent Surfaces This is the DialogTheme capture against the screenshot suite's textured diagonal-stripe backdrop. The backdrop is intentional — it lets reviewers see whether anything that is supposed to be translucent actually is. The iOS Modern Dialog uses an rgba surface fill (0.78 alpha in light, 0.95 in dark — dark needs more opacity because bright stripes bleed through) and its DialogBody, DialogTitle, ContentPane, CommandArea sub-UIIDs are transparent, so the rounded corners read cleanly. The same trick is applied to TabsContainer and the iOS MultiButton. Runtime Palette Overrides The native theme is meant to be a starting point — you can layer your own palette on top without forking the theme. Above is the PaletteOverrideTheme capture: the base is iOS Modern, but the test layers a magenta palette on top at runtime via UIManager.addThemeProps(...). RaisedButton, FlatButton, the disabled tone, and the body-copy span all pick up the override in both light and dark — the override seam works at the resource-bundle layer, exactly the same mechanism a user theme uses to override the native theme on a real app. In the Simulator Three pieces, all live: Themes are bundled. The simulator jar-with-dependencies includes both modern themes alongside the four legacy themes (iPhoneTheme, iOS7Theme, androidTheme, android_holo_light) at the root of the jar. The simulator can pick any one of them at runtime without touching the skin repo.A new "Native Theme" menu. Right next to the Skins menu, there is now a Native Theme menu with a radio group for the six themes, plus "Auto" and "Use skin's embedded theme". Selecting one writes the simulatorNativeTheme Preference, flips the simulator-reload flag, and disposes the current window so the skin reloader kicks in with the new theme. You can sit on a single skin and flip through every native theme in seconds.Build hints know about it. The new nativeTheme, ios.themeMode, and and.themeMode build hints are registered with the simulator's Build Hints UI on launch — labels, types, value lists, descriptions, the lot. (The legacy keys cn1.nativeTheme and cn1.androidTheme are still honored for back-compat.) Set them in the Build Hints dialog, in codenameone_settings.properties, or via -D system properties; they flow through to the device build and the simulator, both. The "Auto" choice in the Native Theme menu defers to those build hints — set ios.themeMode=modern in your project's settings and "Auto" previews iOS Modern; flip the same project to ios.themeMode=ios7 and "Auto" previews iOS 7. The explicit menu entries (iOS Modern, iOS 7, etc.) override the hints regardless. -Dcn1.forceSimulatorTheme is still honored as the highest-priority override; pick "Use skin's embedded theme" to bypass the framework theme entirely and get whatever the skin shipped with. On Devices The opt-in is the same on iOS and Android. The platform knobs follow a single naming pattern — ios.themeMode and and.themeMode — and accept modern / liquid / auto / ios7 / flat on iOS, modern / material / auto / hololight / legacy on Android. There is a single cross-platform shortcut, nativeTheme=modern, which the iOS builder consults when ios.themeMode is unset and which the Android port reads at runtime as a default for and.themeMode. The legacy aliases cn1.androidTheme and cn1.nativeTheme are still honored for back-compat, as is and.hololight=true. The default for an existing app stays on legacy on every platform. We do not flip a 15-year-old app's look without an opt-in. New apps generated from the initializr ship with nativeTheme=modern, ios.themeMode=modern, and and.themeMode=modern already set in codenameone_settings.properties, so a brand new project starts with the modern themes preselected. The Playground does the same, and Playground project downloads carry the same defaults into the generated codenameone_settings.properties. The HTML5 port has the runtime support for the modern themes, but does not bundle them with user apps yet — that is one of the loose ends we want to close in the next round. Sticky Headers The other piece of look-and-feel that we want to highlight is StickyHeaderContainer, which finally has a proper home in the framework. It is the iOS-contacts-list / sectioned-material-list component: scroll past a section boundary, and the previous header is replaced by the next one. New this week, the swap is animated. A directional slide moves the outgoing header up on a forward scroll and down on a reverse scroll, or you can pick a cross-fade. Above is a six-frame sweep from the screenshot test — the user scrolls through sections A, B, C, D, E, and the pinned header recolors to whichever section is currently active at the top of the viewport. The API is small. Build the container, register sections with addSection(header, content), configure the transition style and duration, and add it to a Form: Java StickyHeaderContainer sticky = new StickyHeaderContainer(); sticky.setTransitionStyle(StickyHeaderContainer.TRANSITION_SLIDE); sticky.setTransitionDurationMillis(250); for (char c = 'A'; c <= 'Z'; c++) { Label header = new Label("" + c, "StickyHeader"); Container items = new Container(BoxLayout.y()); for (int i = 0; i < 5; i++) { items.add(new Label(c + " entry " + i)); } sticky.addSection(header, items); } TRANSITION_SLIDE is the default. TRANSITION_FADE cross-fades the outgoing header on top of the incoming one. TRANSITION_NONE keeps the prior instantaneous swap if you want it. Issue #4807 for the original request. How We Test This Every screenshot in this post is captured by a test that runs the app on a real iOS device, an Android emulator, and headless Chrome, then diffs each capture against a stored golden image. The diff is the test — if the rendered pixels drift, the run fails. For animations, the test grabs a series of frames over a fixed-duration transition, then composites them into a single index image. That is how the dual-appearance shots end up as one side-by-side picture per test: … and how the sticky-header animation ends up as a six-frame strip stitched into a GIF: If you want to read the source, the suite lives at scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/. Bugs and Misc Features From This Week The theme work was the loudest thing this week, but plenty of other commits landed alongside it: SIMD large-allocation fallback. The SIMD path on iOS allocates its working buffers on the stack via alloca for speed. Past a certain buffer size, the stack allocation simply fails — there is not enough stack to give, and the request crashes the process. The fix detects that case and falls back to a regular heap allocation when the request is too large to live on the stack. Small SIMD ops keep the fast alloca path; large ones no longer crash.Pluggable AnimationTime clock. Motion, Timeline, MorphAnimation, Image.animate, and Label tickers now all route through a new AnimationTime class that defaults to System.currentTimeMillis() but can be overridden. Tests can drive animations deterministically frame by frame; demos can run in slow motion or fast forward; Motion.slowMotion is no longer the only lever.POSIX character classes for non-ASCII letters. [[:alpha:]], [[:alnum:]], [[:lower:]], and [[:upper:]] silently failed to match anything outside the basic ASCII range — Greek, Cyrillic, CJK ideographs, accented letters, vulgar fractions, currency symbols. They now match the way you would expect, with five regression tests covering the failing cases from the issue.Fail-fast on JDK < 11. The simulator and "Run as desktop app" goals fork the JVM with --add-exports=java.desktop/com.apple.eawt=ALL-UNNAMED, which JDK 8 rejects with the unhelpful "Could not create the Java Virtual Machine". Now the Maven plugin checks the runtime JDK version on entry to cn1:run and cn1:debug and aborts with a friendly message naming the detected version, JAVA_HOME, and a pointer to Adoptium. JDK 11 through 25 is the supported runtime range for the simulator, JDK 8 stays the build-time requirement for the core framework, and JDK 8 is still fully supported at runtime for shipped desktop apps — only the simulator / "Run as desktop app" Maven goals require JDK 11+.Sheet scrolling, swipe, and animation. Sheet finally drags from the bottom with a real animation instead of snapping in. Issue #4825.Picker positioning. Picker got additional button-positioning options and a small batch of coverage tests.Playground polish. The Playground moved every Dialog.show(...) to InteractionDialog mode so user code calling Dialog.show does not blow away the editor chrome — it renders into the layered pane instead. Error messages got a substantial overhaul. The preview-resolution syntax expanded so the Playground can pick previews from a much wider set of expressions, with a new harness keeping it honest in CI.Deeper refreshTheme(). Form.refreshTheme() has been around forever — it re-resolves the styles on a single Form. The new thing this week is UIManager.getInstance().refreshTheme(), which snapshots the current theme props and theme constants, clears the resolved-style caches, and re-applies the lot. This is what lets the screenshot suite flip dark mode mid-suite and see fresh styles, and what lets a runtime palette override take effect immediately. Most apps will never need to call it directly — palettes typically don't change at runtime, and a Display.setDarkMode(...) call already triggers the right invalidation. It is there if you do change the palette and want the change to stick on the next paint without reloading the theme from disk. Where This Is Going — and a Thank-You Last week's post was about Codename One feeling faster: corrected pixel densities, principled scroll physics, SIMD on iOS, and accessibility text scaling. This week is the symbiotic other half — Codename One, looking like it belongs on a 2026 phone. Both halves are the same project. There is not much point in shipping a SIMD-accelerated Base64 if the surrounding UI looks like a 2014 app, and there is not much point in shipping a glass-frosted Dialog if the scroll underneath it judders. Neither half is finished. They are both ongoing, and they both depend on community help — bug reports, RFEs, the patient back-and-forth on issue threads where somebody describes a layout problem on an iPhone you do not own. A specific thank you to the people who drove the issues that turned into this week's commits: Thomas (@ThomasH99) filed #4781 (the original "build a liquid glass example" RFE that started this whole effort), #4807 (sticky headers), #4838 (sideways tab swipe), #4841 (the POSIX regex fix), #4819 (picker buttons), and several others; Francesco Galgani (@jsfan3) filed #4825 (sheet swipe animation) and #4824 (light + dark theme by default in initializr); @ddyer0 caught #4811 (the EDT stack overflow) and #4767 (iPad restart Form size); Lucca Biagi (@LuccaPrado) filed #4817 (form creation in IntelliJ). Several of those are RFEs you would not file unless you actually use the framework day-to-day, and that is the kind of feedback that turns into shippable work. We are sitting at 496 open issues as of this post. That is slow but steady progress — the number is moving in the right direction week over week, and the issues that close tend to ship as features or fixes you can see, not as silent triage. If you have a problem, file it. If you have an RFE, file that too. The themes you saw above started as an RFE. You can try the new themes today by opening the Playground by setting nativeTheme=modern (or ios.themeMode=modern / and.themeMode=modern for finer control) in your project's codenameone_settings.properties, or by picking them from the simulator's new Native Theme menu. New projects from the initializr already have them on. The shipping resources are bundled in the iOS and Android ports as of this week.
This post walks through building and running a real-world agentic workflow with Agentican and Quarkus. Specifically, an agentic workflow to automate market research and information sharing: Identify the top vendors within a market category.Research the positioning and strengths of each vendor.Classify the findings as either standard or urgent.Draft a brief to share with others in the company. Prerequisites QuarkusJava 25Maven (or Gradle)LLM provider API key Step 1: Add the dependency Create a Quarkus app, and add the Agentican Quarkus runtime module: XML <dependency> <groupId>ai.agentican</groupId> <artifactId>agentican-quarkus-runtime</artifactId> <version>0.1.0-alpha.3</version> </dependency> Step 2: Define Agents, Skills, and the Workflow Create an `agentican-catalog.yaml` file on the classpath. This is where you describe: Who does the work (agents)What they need to do it (skills)How they will do it (workflows) YAML agents: - id: researcher name: researcher role: | Expert at finding accurate, sourced information about companies and markets. Quotes sources. Distinguishes opinion from fact. - id: writer name: writer role: | Synthesizes research into structured, concise briefs. Avoids hedging language. Cites concrete evidence. skills: - id: web-search name: web-search instructions: | When a question requires external information, call the search tool first. Quote sources in your answer. Update the `agentican-catalog.yaml` file to define the workflow. YAML workflows: - id: market-brief name: market-brief description: Research vendors in a market and produce a structured brief outputStep: deliver params: - name: topic description: Market to research required: true - name: vendor_count description: Number of vendors defaultValue: "5" steps: - name: identify agent: researcher skills: [web-search] instructions: | Identify the top {{param.vendor_count} vendors in {{param.topic}. Return a JSON array of vendor names — names only, no commentary. - name: deep-dive type: loop over: identify steps: - name: analyze agent: researcher skills: [web-search] instructions: | Deep-dive vendor {{item}: positioning, key strengths, recent news. Quote sources. - name: classify agent: writer instructions: | Read the per-vendor deep-dives below. If any vendor has launched a competitive feature in the last 30 days, return the single word 'urgent'. Otherwise return 'standard'. Deep-dives: {{step.deep-dive.output} dependencies: [deep-dive] - name: deliver type: branch from: classify default: standard branches: - name: urgent steps: - name: urgent-brief agent: writer instructions: | Synthesize a vendor brief flagged URGENT for executive review. Lead with the recent competitive moves. Topic: {{param.topic} Deep-dives: {{step.deep-dive.output} - name: standard steps: - name: standard-brief agent: writer instructions: | Synthesize a vendor brief. Topic: {{param.topic} Deep-dives: {{step.deep-dive.output} A few things worth flagging: agent: researcher references the agent for a step, skills referenced by name, too.outputStep designates the step whose output becomes the workflow's typed result.{{param.X} interpolates workflow inputs into step instructions.{{step.X.output} interpolates an upstream step's output.{{item} is the current value inside a loop iteration.type: loop steps take an over reference (a step that produced a list, or a list-typed param).type: loop steps run their nested steps once per item, in parallel, and on virtual threads.type: branch steps take a from reference (a step whose output is used to select a branch).branches: mutually exclusive steps (or sets of steps) with default for unrecognized values. The framework loads agentican-catalog.yaml from the classpath, or you can define where it's loaded from: Properties files agentican.catalog-config=/etc/agentican/agentican-catalog.yaml Note: Agents, skills, and workflows can be defined via a fluent builder API as well. Step 3: Configure the Models Agentican reads the engine configuration from `application.properties`. The minimum is one LLM: Properties files agentican.llm[0].api-key=${ANTHROPIC_API_KEY} The provider defaults to `anthropic`, and the model defaults to `claude-sonnet-4-5`. Want OpenAI instead? Properties files agentican.llm[0].provider=openai agentican.llm[0].api-key=${OPENAI_API_KEY} agentican.llm[0].model=gpt-4o-mini Want to mix and match? Configure `name`s and reference them per-agent in the YAML catalog: Properties files agentican.llm[0].name=default agentican.llm[0].api-key=${ANTHROPIC_API_KEY} agentican.llm[1].name=efficient agentican.llm[1].provider=openai agentican.llm[1].api-key=${OPENAI_API_KEY} agentican.llm[1].model=gpt-4o-mini Step 4: Create a Typed Workflow Instance Define the workflow input and output records: Java public record ResearchParams(String topic, int vendorCount) {} public record VendorBrief(String topic, List<Vendor> vendors) { public record Vendor(String name, String positioning, List<String> strengths) {} } Then inject the typed workflow, and call it from a REST endpoint: Java @Path("/market-brief") public class VendorBriefResource { @Inject @AgenticanWorkflow(name = "market-brief") Workflow<ResearchParams, VendorBrief> brief; @POST @Path("/{topic}") public VendorBrief generate(@PathParam("topic") String topic) { return brief.start(new ResearchParams(topic, 5)).await(); } } Now, test the endpoint: Shell curl -X POST http://localhost:8080/market-brief/data%20observability%20platforms A few things worth flagging — they're what set this apart from a generic "call an LLM" library: ResearchParams.vendorCount becomes the workflow parameter vendor_count via SNAKE_CASE mapping.start() returns a WorkflowRun<VendorBrief> and await() parses the output step's text into a VendorBrief.@AgenticanWorkflow(name = "vendor-brief") resolves the registered workflow at injection time. Note: WorkflowRun itself exposes future() for a CompletableFuture<R>, and there's a ReactiveWorkflow<P, R> Mutiny variant for Vert.x stacks. Step 5: Add Agent Tools Agentican ships two integrations out of the box: MCP (Model Context Protocol) There is one config block per server. Tools are auto-discovered: Properties files agentican.mcp[0].slug=github agentican.mcp[0].name=GitHub agentican.mcp[0].url=https://mcp.github.com/sse agentican.mcp[0].headers.Authorization=Bearer ${GITHUB_TOKEN} Composio 100+ SaaS toolkits — Slack, Notion, Linear, Salesforce, GitHub, Google Workspace: Properties files agentican.composio.api-key=${COMPOSIO_API_KEY} agentican.composio.user-id=user-123 Tools are referenced by name within agent steps: YAML steps: - name: research agent: researcher tools: [github_search_repositories] instructions: "Profile open-source vendors in {{param.topic}." Structured agentic workflows for the JVM. Where to Go Next Getting Started — install, configure, and run workflowsCore Concepts — architecture, terminology, and data flowWorkflows & Steps — CDI surface, beans, qualifiers, override patterns.Agents — defining agents, skills, and rolesGetting Started (Quarkus) — dependency setup, config, first taskCDI Integration — injection, qualifiers, lifecycle events, bean overridesREST API — endpoints, SSE streaming, WebSocket, error codesObservability — Micrometer metrics, OTel tracing, Prometheus queries
Artificial intelligence is rapidly transforming software development. Many developers now use AI-powered tools to generate code, but the next advancement is integrating AI directly into applications. Modern systems increasingly use large language models (LLMs) to answer questions, automate workflows, summarize information, and enhance user experiences. Software engineers must therefore combine traditional enterprise development practices with AI capabilities while ensuring reliability, scalability, and maintainability. This evolution offers Jakarta EE developers a significant opportunity. Jakarta EE provides a mature platform for enterprise applications, with standards for dependency injection, RESTful services, configuration, persistence, and cloud-native development. By integrating Jakarta EE with LangChain4j, developers can access advanced AI models through a straightforward Java API, adding intelligent features without leaving the familiar Jakarta EE environment. In this article, we will build a simple "Hello World" AI application to demonstrate how easily a Large Language Model can be integrated into a Jakarta EE application using LangChain4j. Configuring LangChain4j With Jakarta EE Technologies Before developing your first AI-powered application, it is important to understand LangChain4j’s role in the Java ecosystem and its popularity for AI integration. LangChain4j serves as an orchestration layer between Java applications and AI providers. It simplifies AI integration by offering a consistent programming model, regardless of the underlying vendor. If you are familiar with Spring Data or Jakarta Data, this concept will be familiar. With Spring Data and Jakarta Data, developers define repository interfaces and use annotations to specify behavior. Implementation details are handled by a provider that generates the concrete implementation and manages database communication. This allows developers to focus on business logic rather than low-level database operations. LangChain4j uses a similar approach for artificial intelligence. Instead of writing HTTP clients, building JSON payloads, and managing provider-specific APIs, developers define Java interfaces representing AI capabilities. LangChain4j then generates the implementation and manages communication with the chosen AI provider. LangChain4j can be viewed as the AI equivalent of Jakarta Data or Spring Data, with the AI provider dependency functioning like a JDBC driver. Switching from one AI provider to another, such as from OpenAI to a different provider, usually only requires updating the dependency and configuration, while the application code remains largely unchanged. While this article uses a Java SE application for simplicity, the same approach applies to Jakarta EE, Spring Boot, Quarkus, Helidon, Micronaut, and other Java platforms. Project Dependencies The first step is to create a Maven Quickstart project and add the required dependencies for CDI, Eclipse MicroProfile Config, and LangChain4j: XML <dependency> <groupId>io.smallrye.config</groupId> <artifactId>smallrye-config-core</artifactId> <version>3.17.2</version> <scope>compile</scope> </dependency> <dependency> <groupId>io.smallrye.config</groupId> <artifactId>smallrye-config</artifactId> <version>3.17.2</version> </dependency> <dependency> <groupId>org.jboss.weld.se</groupId> <artifactId>weld-se-core</artifactId> <version>6.0.4.Final</version> </dependency> <dependency> <groupId>dev.langchain4j.cdi</groupId> <artifactId>langchain4j-cdi-portable-ext</artifactId> <version>${langchain4j-cdi.version}</version> </dependency> <dependency> <groupId>dev.langchain4j.cdi.mp</groupId> <artifactId>langchain4j-cdi-config</artifactId> <version>${langchain4j-cdi.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>1.15.0</version> </dependency> This example uses the langchain4j-open-ai dependency, which serves as the provider-specific driver for communicating with OpenAI models. The application code remains independent of the provider implementation. Configuring the AI Provider LangChain4j integrates with Eclipse MicroProfile Config, allowing you to externalize all provider settings. Create a microprofile-config.properties file and add the following configuration: Properties files dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.openai.OpenAiChatModel dev.langchain4j.cdi.plugin.chat-model.config.api-key=<<API_KEY>> dev.langchain4j.cdi.plugin.chat-model.config.model-name=gpt-5 This configuration specifies the chat model implementation, the authentication API key, and the model that will process prompts. A key advantage of this approach is flexibility. If you choose another provider in the future, you typically only need to replace the provider dependency and update the configuration. The application code often remains unchanged, reinforcing the provider dependency’s role as similar to that of a JDBC driver in traditional data access. For this sample, you can place the API key directly in the configuration file or provide it through environment variables. In production, use environment variables, secret managers, or vault solutions. Never commit API keys to source control, as exposed credentials can lead to unauthorized use, unexpected costs, and security risks. Your First AI Service With the project configured, we can now build our first AI-powered service. As is customary in software development, we will begin with a “Hello World” example. Rather than printing a static message, we will send a question to an AI model and display its response. This example uses the simplest contract: a String as input and a String as output. Although real-world applications typically use more complex domain objects, starting with plain text helps us focus on the core LangChain4j programming model and understand how to create and use AI services. The first step is defining an AI service interface: Java import dev.langchain4j.cdi.spi.RegisterAIService; import jakarta.enterprise.context.ApplicationScoped; @RegisterAIService @ApplicationScoped public interface AssistantService { String chat(String prompt); } This interface does not include an implementation. LangChain4j generates the implementation automatically at runtime. The @RegisterAIService annotation directs LangChain4j to create an AI-backed implementation for this interface. The @ApplicationScoped annotation makes the generated implementation available as a CDI bean, which can be injected or accessed like any other Jakarta EE component. The method signature defines the AI contract. When the chat method is called, the parameter serves as the prompt for the AI model, and the returned value contains the generated response. In this example, both the request and response are simple strings. Next, we need a client application to consume this service: Java import jakarta.enterprise.context.control.RequestContextController; import jakarta.enterprise.inject.se.SeContainer; public class App { public static void main(String[] args) { try (SeContainer container = jakarta.enterprise.inject.se.SeContainerInitializer .newInstance() .initialize()) { RequestContextController requestContextController = container.select(RequestContextController.class).get(); requestContextController.activate(); AssistantService assistantService = container.select(AssistantService.class).get(); String response = assistantService.chat("What is the capital of France?"); System.out.println("Assistant response: " + response); requestContextController.deactivate(); } } } The application starts a CDI container using Weld SE, which provides dependency injection in a Java SE environment. After initializing the container, we activate the request context and obtain an instance of AssistantService from CDI. Although there is no concrete implementation in the codebase, CDI returns a fully functional service generated by LangChain4j. When the chat method is called, LangChain4j sends the prompt to the configured AI model, waits for the response, and converts the result into a Java String. Running the application produces an output similar to the following: Plain Text Assistant response: Paris is the capital of France. The exact wording may vary because large language models are probabilistic systems. Unlike traditional methods that always return the same result for a given input, AI models may produce slightly different responses while maintaining the same meaning. While using strings is useful for learning the fundamentals, enterprise applications rarely exchange raw text between layers. Business applications typically use structured data, domain objects, commands, and responses to ensure stronger contracts and better maintainability. In the next section, we will enhance this example by replacing raw strings with dedicated input and output classes, enabling LangChain4j to map between Java objects and AI interactions in a more type-safe and expressive manner. Working With Structured Input and Output The previous example showed a basic AI interaction: a string input produces a string output. While this illustrates the fundamentals, real-world applications rarely use unstructured text alone. Enterprise systems typically exchange well-defined objects that represent business concepts, making code more expressive, maintainable, and type-safe. LangChain4j’s key strength is its ability to map Java objects directly to AI interactions. It automatically converts structured input into prompts and transforms AI responses into strongly typed Java objects, eliminating the need for manual serialization and parsing. Developers can work with domain concepts instead of raw text. To demonstrate this, we will build a simple book recommendation engine. Given a book title and author, the AI will suggest three books that logically follow in a learning journey. We begin by defining the input object: Java public record BookRequest(String title, String author) { } This record captures the user’s input. Instead of manually creating a textual prompt, we provide a structured Java object with the book’s title and author. Next, we define the domain model representing a recommended book: Java import java.util.List; public record Book( String title, String author, String description, List<String> keywords) { } This record contains richer information than a simple title. This record includes more than just the title and author. It also provides a short description and a set of keywords to further characterize the recommendation, with the reason why the book was selected: Java public record Recommendation(Book book, String reason) { } Finally, we create a wrapper object that represents the complete response returned by the AI service: Java import java.util.List; public record NextReadBooks(List<Recommendation> recommendations) { } At this stage, we have a complete domain model for both the request and the expected response. Next, we define the AI service: Java import dev.langchain4j.cdi.spi.RegisterAIService; import dev.langchain4j.service.SystemMessage; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped @RegisterAIService public interface NextReadBookService { @SystemMessage(""" Recommend up to 3 books that should naturally follow the provided book in a learning journey. Recommendations should prioritize: - conceptual progression - complementary knowledge - technical depth - thematic similarity For each recommendation provide: - title - author - concise description - relevant keywords - a short recommendation reason Keep recommendations concise, technically relevant, and focused on software engineering and architecture learning. """) NextReadBooks nextReadBooks(BookRequest bookRequest); } This example also introduces the concept of a system message. The @SystemMessage annotation provides instructions that guide the model’s behavior. Unlike user input, which varies with each request, the system message serves as a permanent set of rules for AI responses. Here, we instruct the model to recommend up to three books, explain each recommendation, and return the information using our defined Java records. The method signature uses only domain objects: BookRequest as input and NextReadBooks as output. There is no need for manual JSON handling, prompt creation, or response parsing, as LangChain4j manages these tasks automatically. The application code remains straightforward: Java import jakarta.enterprise.context.control.RequestContextController; import jakarta.enterprise.inject.se.SeContainer; public class BookApp { public static void main(String[] args) { try (SeContainer container = jakarta.enterprise.inject.se.SeContainerInitializer .newInstance() .initialize()) { RequestContextController requestContextController = container.select(RequestContextController.class).get(); requestContextController.activate(); var bookService = container.select(NextReadBookService.class).get(); BookRequest request = new BookRequest( "The Great Gatsby", "F. Scott Fitzgerald"); var recommendations = bookService.nextReadBooks(request); for (var recommendation : recommendations.recommendations()) { System.out.println( "Recommended book: " + recommendation.book().title() + " by " + recommendation.book().author()); System.out.println( "Reason: " + recommendation.reason()); } requestContextController.deactivate(); } } } When executed, LangChain4j converts the BookRequest into a prompt, sends it to the model, validates the response against the target structure, and maps the result back into NextReadBooks. For developers, this interaction is similar to calling a standard Java service. This approach offers clear advantages over raw string-based interactions. The code is easier to understand, IDE autocompletion enhances productivity, and refactoring is safer because inputs and outputs are explicit domain models. The application can also adapt more easily to new business requirements. So far, our examples have used explicit user requests and static system instructions. However, modern AI applications often need additional context beyond user input. In the next section, we will explore how to enrich AI interactions with external knowledge and context, enabling the model to produce more accurate and relevant responses aligned with the application’s domain.
Java structured concurrency has been under development for a span of 5 years, weaving through 8 (!) distinct JEPs (JEP 428, JEP 437, JEP 453, JEP 462, JEP 480, JEP 499, JEP 505, JEP 525). To me, this feels rather excessive for what could be considered a fairly concise feature. My goal here is to experiment with an alternative approach that leverages Java's tried-and-tested, robust functionality available since JDK 1.5. It's possible this pathway could achieve better outcomes than what is proposed in JEP 505, which, from my perspective, introduces a suite of redundant interfaces and classes that replicate pre-existing ones. No doubt, developers need some governance, even in a relatively safe development environment like Java, with its automatic garbage collection, memory management, and strict typing. No matter how safe the provided path is, developers will still make mistakes, such as dereferencing nulls, using out-of-bound indexes, swallowing exceptions, and who knows what else. And, undoubtedly, concurrency is the hardest thing to get right — it's an endless source of bugs. But first, let me introduce some helper code that we will use throughout the article. Java // Example Proto package net.tascalate.concurrentx; // imports here public class FuturesDemo { static final ScopedValue<String> DEMO_SV = ScopedValue.newInstance(); // This emulates long-running calls // we need to execute asynchronously -- // all we do is returning value after the delay // or throw a supplied exception to emulate error private static <T> Callable<T> produceValue(T value, long delay) { return () -> { var start = System.currentTimeMillis(); try { System.out.println(">> Waiting value: " + value + " (SCOPED VALIUE IS " + DEMO_SV.orElse("<UNBOUND>") + ")"); Thread.sleep(delay); System.out.println(">> Producing value: " + value); if (value instanceof Exception) { throw (Exception)value; } else { return value; } } finally { var finish = System.currentTimeMillis(); System.out.println(">> Exiting " + value + ", " + Thread.currentThread() + ", done in " + (finish - start) + "ms, vs " + delay + "ms specified"); } }; } public static void main(String[] argv) { // implementation will be here } } According to Oracle, the majority of Java developers tend to approach concurrency execution in the following way (excerpt courtesy JEP 505, modified to use a helper code from above): Java // Example A - "unstructured concurrency" public static void main(String[] argv) throws InterruptedException, ExecutionException { var executor = Executors.newVirtualThreadPerTaskExecutor(); var start = System.currentTimeMillis(); try { Future<String> a = executor.submit( produceValue("A", 1000)); Future<LocalDateTime> b = executor.submit( produceValue(LocalDateTime.now(), 1500)); Future<BigInteger> c = executor.submit( produceValue(BigInteger.valueOf(42), 500)); var result = List.of(a.get(), b.get(), c.get()); System.out.println("*** ALL result: " + result); } finally { var finish = System.currentTimeMillis(); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); executor.shutdownNow(); } } Here, a range of critical problems lurk, several of which are detailed in the "Motivation" section of the JEP: In contrast to the above example, Oracle proposes the use of its structured concurrency API as a solution that, hypothetically, addresses these concerns: Java // Example B -- structured concurrency @SuppressWarnings("preview") public static void main(String[] argv) throws InterruptedException, ExecutionException { var start = System.currentTimeMillis(); try (var scope = StructuredTaskScope.open( StructuredTaskScope.Joiner.allSuccessfulOrThrow())) { var a = scope.fork(produceValue("A", 1000)); var b = scope.fork(produceValue(LocalDateTime.now(), 1500)); var c = scope.fork(produceValue(BigInteger.valueOf(42), 500)); scope.join(); var result = List.of(a.get(), b.get(), c.get()); System.out.println("*** ALL result: " + result); } catch (StructuredTaskScope.FailedException ex) { System.out.println("*** ALL exception: " + ex.getCause()); } finally { var finish = System.currentTimeMillis(); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); } } Let’s shift our focus back to the original code. After putting in diligent QA efforts, writing useful tests with good code coverage, and completing a thorough code review, what’s the developer’s next move? Most likely, they'll refine the initial code block to resemble the updated version below: Java // Example C - fixed "unstructured concurrency" from Example A public static void main(String[] argv) throws InterruptedException, ExecutionException { Future<String> a = null; Future<LocalDateTime> b = null; Future<BigInteger> c = null; var executor = Executors.newVirtualThreadPerTaskExecutor(); var start = System.currentTimeMillis(); try { a = executor.submit(produceValue("A", 1000)); b = executor.submit(produceValue(LocalDateTime.now(), 1500)); c = executor.submit(produceValue(BigInteger.valueOf(42), 500)); var result = List.of(a.get(), b.get(), c.get()); System.out.println("ALL result: " + result); } finally { var finish = System.currentTimeMillis(); Stream.of(a, b, c) .filter(Objects::nonNull) .forEach(f -> f.cancel(true)); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); executor.shutdownNow(); } } At a glance, this approach seems fairly effective — any remaining Features are canceled in the instance of an intermediate error, and all execution threads are properly terminated. However, there's still a fair amount of boilerplate code, which remains cumbersome to implement consistently. No problem, let's extract common functionality into some reusable class. Please see the TaskScope class in the Gist. By doing so, the code undergoes a noticeable transformation: Java // Example D - fixed "unstructured concurrency" from Example A // with a reusable TaskScope class public static void main(String[] argv) throws InterruptedException, ExecutionException { var start = System.currentTimeMillis(); try (var scope = new TaskScope( Executors.newVirtualThreadPerTaskExecutor())) { var a = scope.fork(produceValue("A", 1000)); var b = scope.fork(produceValue(LocalDateTime.now(), 1500)); var c = scope.fork(produceValue(BigInteger.valueOf(42), 500)); var result = List.of(a.get(), b.get(), c.get()); System.out.println("*** ALL result: " + result); } finally { var finish = System.currentTimeMillis(); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); } } Upon inspecting the Gist sources — which you absolutely should for understanding — you’ll notice something important: this implementation relies on Java version 1.8, released over 12 years ago. Furthermore, if it does not use java/util/stream/Stream, it can even run seamlessly on JDK 1.5! But hold on — why incorporate java/util/stream/Stream here? Quite frankly, it's the core of the proposal. Take example D above: it efficiently handles just one scenario, namely, waiting for all tasks to finish while throwing an error if any fail along the way. Support for different scenarios requires something a bit more sophisticated. The TaskScope implementation shared in the Gist translates a queue of completed Futures (irrespective of whether completion came via a result, error, or cancellation) directly into a Stream. Curious why this may be useful? Let's rewrite this boring example once again: Java // Example E - same as Example D but with Stream pipeline public static void main(String[] argv) { var start = System.currentTimeMillis(); try (var scope = new TaskScope( Executors.newVirtualThreadPerTaskExecutor())) { scope.fork(produceValue("A", 1000)); scope.fork(produceValue(LocalDateTime.now(), 1500)); scope.fork(produceValue(BigInteger.valueOf(42), 500)); var result = scope.completions() .map(Future::resultNow) .toList(); System.out.println("*** ALL result: " + result); } finally { var finish = System.currentTimeMillis(); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); } } This way, we just convert all the completed features into the list of results and keep our fingers crossed that there were no errors. Let’s turn all successfully completed futures into a result list, disregarding potential errors entirely. No exceptions will ever be thrown within this scope: Java var result = scope.completions() .filter(f -> f.state() == Future.State.SUCCESS) .map(Future::resultNow) .toList(); Or simply find the first result available: Java var result = scope.completions() .filter(f -> f.state() == Future.State.SUCCESS) .map(Future::resultNow) .findAny() .orElse("<NONE>"); Or, alternatively, select no more than the first N results: Java var N = 5; var result = scope.completions() .filter(f -> f.state() == Future.State.SUCCESS) .map(Future::resultNow) .limit(N) .toList(); In these two recent examples, any remaining futures will automatically be terminated once the try-with-resources block in the main method exits. Clearly, we can also handle errors while gathering results and terminate prematurely — if the code logic doesn't permit intermediate errors: Java var result = scope.completions() .peek(f -> { if (f.state() == Future.State.FAILED) throw new CompletionException(f.exceptionNow()); }) .map(Future::resultNow) .limit(2) .toList(); If you're already acquainted with JEP 505, you’ll understand what is being replaced here: StructuredTaskScope.Joiner. Now, you can mimic any type of "join" behavior without the need to subclass/implement StructuredTaskScope.Joiner. The Stream pipeline API over the completions queue serves as an expressive tool to achieve this out of the box. Plus, with the introduction of Gatherers, there’s room for truly ad hoc scenarios, such as managing result windows — think fixed-size batches of completed results processed as soon as they are ready. It’s also worth noting that in JEP 505, a certain StructuredTaskScope.Joiner implementations produce streams as their output. However, it’s the Joiner that determines when all forks have finished processing and opens the resulting stream post-join. In the alternative methodology described here, the decision of where and how joins occur resides within user-defined scope-flow logic. It’s a lazy, on-demand process — guided by conditions that may take more into account than just Future results. For instance, elements like internal object state or in-scope variables can directly influence decisions about which results to collect and which errors, if any, can be disregarded in the operation. Now to the real challenge. A notable limitation with the code given is its inability to propagate context, namely, the current ScopedValue-s bindings. This characteristic is sometimes cited as a primary strength of JEP 505 StructuredTaskScope. To be fair, one might argue it's an unfair advantage, one that exists solely because JDK-internal mechanisms make it achievable. Current bindings are captured and propagated by using jdk/internal/misc/ThreadFlock — a utility inaccessible to code outside of the JDK. Perhaps, in a more ideal universe, there is a JDK 25, equipped with the following official API for java/util/concurrent/ThreadFactory, introducing possibilities for bridging this gap: Java public interface ThreadFactory { abstract Thread newThread(Runnable code); default ThreadFactory captureContext() { ThreadFactory delegate = this; Object currentScopedValueBindings = SomeInternalClass.captureValueBindingsForTheCurrentThread(); return new ThreadFactory() { public Thread newThread(Runnable code) { Thread result = delegate.newThread(code); SomeInternalClass.applyValueBindings(result); return result; } }; } } But that's not the case for us. Thankfully, the classes from the java/util/concurrent package offer immense customizability and are remarkably adaptable tools (a big nod to Dr. Douglas S. Lea for this). So you can find another class, TaskScopeContextual, in the same Gist. This class adopts StructuredTaskScope to the ExecutorService API, solely aimed at promoting ScopedValue bindings for forked tasks. The following example highlights all the advantages of employing this alternative structured scope design: Java // Example F - true structured concurrency with context passing public static void main(String[] argv) { var start = System.currentTimeMillis(); ScopedValue.where(DEMO_SV, "VALUE_DEFINED_IN_MAIN").call(() -> { try (var scope = new TaskScopeContextual()) { scope.fork(produceValue("A", 1000)); scope.fork(produceValue("B", 2000)); scope.fork(produceValue("C", 2000)); scope.fork(produceValue("D", 2000)); var timeout = scope.fork(produceValue(null, 2500)); scope.fork(produceValue("E", 2000)); scope.fork(produceValue("F", 3000)); scope.fork(produceValue("G", 3000)); var result = scope.completions() .takeWhile(f -> f != timeout) .filter(f -> f.state() == Future.State.SUCCESS) .limit(6) .map(Future::resultNow) .sorted() .toList(); System.out.println("*** ALL result: " + result); } finally { var finish = System.currentTimeMillis(); System.out.println( "*** Exiting main, executed in " + (finish - start) + "ms"); } return null; }); } Take note of the elegant handling of timeouts with Streams. Unlike the current approach in JEP 505, there's no necessity to incorporate it into the API. In summary, here’s a recap: There's no requirement for StructuredTaskScope.Subtask — the existing java/util/concurrent/Future API already does the job adequately. Consequently, the inclusion of StructuredTaskScope.Subtask.State is redundant — even with the current JEP 505, Future.State is more than sufficient. StructuredTaskScope.Joiners demand subclassing for all but the simplest cases. A java/util/stream/Stream pipeline over the completed futures would serve as a much more convenient solution. The StructuredTaskScope.FailedException feels unnecessary — even in the current API, java/util/concurrent/CompletionException fulfills the same purpose just fine. Built-in StructuredTaskScope timeouts possess timing characteristics that are challenging to predict (e.g., try adding lengthy blocking calls before the initial fork). It's far simpler and more controlled to handle timeouts explicitly. I'm really interested to hear readers' opinions. Do you share my ideas or do you support JDK team's statement that Futures "are counterproductive in structured concurrency" (see the "Alternatives" section of JEP 505)? Would you say that the well-known and adaptable Stream API is superior to Joiners or strict set of Joiners is simpler?
For decades, Jakarta EE has addressed the challenge of building enterprise systems that endure technological change. The platform has evolved from monoliths to microservices, from application servers to Kubernetes, and from relational databases to distributed data platforms, all while maintaining its core strength: compatibility. Jakarta EE 12 marks another significant transition, shifting the focus beyond cloud-native infrastructure and APIs to prioritize data. Modern enterprise systems now operate in diverse environments that extend beyond relational databases and synchronous CRUD applications. Current architectures integrate SQL, document databases, graph engines, key-value stores, event streams, vector databases, and AI-driven workflows. The primary challenge is to provide a unified programming model that manages fragmented data ecosystems without vendor lock-in or frequent application rewrites. Jakarta EE 12 addresses this by elevating querying, data access, initialization, and semantic consistency to platform-level concerns. This release marks the beginning of the “Data Age” for Enterprise Java. Central to this evolution is Jakarta Query, a unified semantic query model that connects Jakarta Persistence, Jakarta Data, and Jakarta NoSQL through a common abstraction. Rather than having each specification define its own querying semantics, Jakarta EE 12 introduces a shared language that spans multiple persistence technologies while supporting specialized execution models. This architectural shift reduces ecosystem fragmentation and delivers a more consistent developer experience for polyglot persistence systems. Jakarta EE 12 also extends beyond traditional dependency injection and request processing. CDI now offers more predictable startup and lifecycle management, which is essential for cloud-native deployments, serverless runtimes, AI orchestration, and agent-based architectures. With Java 21 as the new platform baseline, Jakarta EE is positioned as a modern platform that supports long-lived, adaptive systems in a data- and AI-driven world. This article will examine how Jakarta EE 12 transforms the enterprise Java ecosystem through Jakarta Query, Jakarta Data, Jakarta NoSQL, CDI 5.0, Persistence 4.0, and new initiatives such as Jakarta Agentic AI. We will also discuss how these specifications form a unified platform strategy that simplifies enterprise development while maintaining the stability and interoperability that have made Java a leading software ecosystem. The Evolution of Enterprise Complexity Software architecture has consistently evolved to address complexity. Initially, organizations relied on centralized mainframes, where applications, infrastructure, and data resided in a single environment. The shift to client-server and three-tier models introduced distributed systems, separating presentation, business logic, and persistence into distinct layers. Today, cloud-native systems span clusters, distributed networks, containers, Kubernetes, edge devices, and globally replicated databases. Modern enterprise software functions as an ecosystem of interconnected services across infrastructure that developers may not fully control. This evolution has significantly increased the cognitive demands on software engineers and architects. Today’s technology landscape includes a wide array of frameworks, runtimes, databases, APIs, messaging systems, orchestration platforms, and AI-driven tools. Developer experience is now a competitive market, with platforms promising productivity, simplicity, and scalability. Engineers must continually balance trade-offs among performance, consistency, scalability, operational complexity, and vendor lock-in. The industry also faces the “hype effect,” where technologies gain popularity before their long-term impacts are fully understood. As systems became more distributed, architectural styles proliferated. Traditional layered architectures now exist alongside microservices, event-driven systems, CQRS, orchestration platforms, microkernels, and domain-driven designs. Each style addresses specific challenges. Microservices enhance deployment independence, event-driven systems improve scalability and resilience, and CQRS manages complex read-and-write workloads. However, this variety has led to fragmentation. Developers must now master not only programming languages and frameworks, but also distributed systems theory, consistency models, observability, fault tolerance, asynchronous communication, and operational automation. Data complexity has evolved similarly. For decades, enterprise applications relied primarily on relational databases and SQL. Today, organizations use document databases, graph databases, key-value stores, wide-column engines, streaming systems, vector databases, and combinations of these. This trend, known as polyglot persistence, reflects the fact that different data models address different business needs. For example, recommendation engines may require graph traversal, financial systems depend on transactional consistency, and AI systems increasingly use vector similarity search. As a result, enterprise development now extends beyond writing business logic. Engineers must manage distributed architectures, multiple persistence models, cloud-native infrastructure, security, asynchronous communication, and increasingly, AI-driven workflows. In this environment, standards are essential. Growing complexity makes fragmentation a significant long-term risk. Without common abstractions and interoperable APIs, organizations risk costly migrations, vendor lock-in, and operational instability. Jakarta EE 12 addresses these challenges. Instead of treating persistence, querying, dependency injection, and runtime behavior as separate concerns, the platform adopts a unified model for modern distributed systems. Its goal is not to eliminate architectural diversity, but to offer a stable and coherent foundation that supports it. Why Jakarta EE Still Matters Enterprise Java has evolved for nearly three decades. Launched in the late 1990s, Java EE aimed to standardize enterprise application development amid a fragmented landscape of proprietary technologies. The ecosystem progressed from J2EE to Java EE and, now, to Jakarta EE under the Eclipse Foundation. Each transition mirrored broader industry shifts, including the emergence of web applications, distributed systems, cloud-native computing, and AI-driven architectures. Java’s dominance in enterprise environments stems from more than the language itself. Its success lies in uniting two elements that rarely coexist: open standards and open source. Many ecosystems offer only one. Some are open source but lack governance and interoperability. Others provide standards but evolve slowly or lose touch with developer needs. Jakarta EE bridges these worlds, delivering both specification-driven consistency and open-source innovation. Historically, standards have been essential for human scalability. Shared languages enabled cooperation, writing systems preserved knowledge, and standard units like the metric system supported global trade and science. Software faces similar challenges. As systems expand and teams become more distributed, shared abstractions and interoperability are crucial. Standards reduce ambiguity, improve team communication, and allow technologies to evolve without frequent rewrites. This is especially important in enterprise environments, where systems often outlast the technologies used to build them. Enterprise applications are rarely rewritten. Banks, governments, healthcare providers, airlines, and retailers operate systems that may persist for decades while evolving internally. In this context, open standards and open source are strategic choices. They reduce operational lock-in, improve vendor portability, support long-lived systems, and enable incremental modernization rather than risky rewrites. Jakarta EE addresses these needs by not imposing a single architecture, runtime, or deployment model. The platform supports monoliths, modular systems, microservices, reactive architectures, and cloud-native deployments. It integrates seamlessly with modern frameworks and runtimes, including those many developers use daily, often without realizing Jakarta EE specifications underpin them. Technologies such as Spring, Quarkus, Micronaut, Hibernate, Tomcat, and Payara implement, extend, or depend directly on Jakarta EE specifications. This is precisely what makes Jakarta EE uniquely relevant today. In a market flooded with, this unique combination makes Jakarta EE especially relevant today. In a market filled with rapidly changing frameworks and infrastructure trends, Jakarta EE offers stability without stagnation. The platform evolves thoughtfully, maintaining compatibility while adapting to new realities such as cloud-native computing, polyglot persistence, and AI-driven systems. Jakarta EE 11 established a modern foundation, with specifications such as Jakarta Data. Jakarta EE 12 builds on this, moving Enterprise Java into what can be called the Data Age. Jakarta EE 12 and the Rise of Unified Data Access A key change in Jakarta EE 12 is the acknowledgment that data access can no longer be limited to relational databases. Modern enterprise applications now span SQL databases, NoSQL engines, distributed caches, event streams, and AI-focused data stores. The primary challenge has shifted from persistence alone to ensuring consistent developer interaction across diverse data systems. Jakarta EE 12 addresses this by introducing a unified semantic model for querying and data access. Central to this is Jakarta Query, a new abstraction that serves as a common query foundation for Jakarta Persistence, Jakarta Data, and Jakarta NoSQL. Rather than each specification defining separate query semantics, Jakarta Query provides a shared language for filtering, ordering, restrictions, and query composition across multiple persistence technologies. Enterprise Java has evolved through several generations of query languages, from JDBC’s direct SQL focus to JPA’s JPQL and various framework-specific abstractions. These independent developments have led to fragmentation. Jakarta EE 12 seeks to address this by separating semantic intent from execution strategy, enabling developers to use a common conceptual model for queries while allowing each technology to optimize execution as needed. This is especially important in polyglot persistence architectures. Relational databases optimize joins and transactions, document databases offer schema flexibility, and graph databases emphasize relationship traversal. Jakarta Query does not eliminate these differences but provides a consistent developer experience across technologies, reducing reliance on vendor-specific APIs. Jakarta Data 1.1 exemplifies this approach with its fluent, type-safe query model. Developers can dynamically compose queries using semantic restrictions and ordering rules in Java, rather than relying on string-based query construction. Java List<Product> found = products.findAll( Restrict.all( _Product.type.equalTo(ProductType.PHYSICAL), _Product.price.greaterThan(10.00f), _Product.name.contains("Jakarta") ), Order.by( _Product.price.desc(), _Product.name.asc() ) ); This approach enhances readability and reduces runtime query errors often found in string-based query languages. More importantly, it aligns queries with the domain model, supporting a core principle of domain-driven enterprise applications. Jakarta Data 1.1 also extends the repository model beyond basic CRUD operations. Stateful repositories now include lifecycle-oriented operations, such as persist, merge, refresh, detach, and remove, within their abstractions. Java @Repository public interface Products extends DataRepository<Product, String> { @Persist void add(Product product); @Merge Product merge(Product product); @Remove void remove(Product product); @Refresh void reload(Product product); @Detach void detach(Product product); } This evolution is significant because repositories are no longer just convenience wrappers for persistence operations. They now serve as standardized data access contracts, consistently supporting both query semantics and entity lifecycle management across implementations. More broadly, Jakarta EE 12 is guiding enterprise Java toward a unified data platform. Instead of requiring developers to switch mental models between persistence technologies, the platform unifies how applications express intent for querying, filtering, lifecycle management, and data interaction. As distributed systems and polyglot persistence become more prevalent, this semantic consistency may become a key architectural advantage for Enterprise Java.
Shai Almog
Co-founder at Codename One,
Codename One
Ram Lakshmanan
yCrash - Chief Architect