foojay – a place for friends of OpenJDK https://foojay.io/today/category/tools/ a place for friends of OpenJDK Wed, 03 Jun 2026 07:07:46 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://foojay.io/wp-content/uploads/2020/04/Favicon-3-2-150x150.png foojay – a place for friends of OpenJDK https://foojay.io/today/category/tools/ 32 32 Why Enterprise Java Teams Need Quality Gates Even More in the Age of AI https://foojay.io/today/enterprise-java-quality-gates-ai/ https://foojay.io/today/enterprise-java-quality-gates-ai/#respond Fri, 29 May 2026 07:00:00 +0000 https://foojay.io/?p=123954 Table of Contents Enterprise quality is a scaling problemLocal differences become delivery problemsNoisy diffs hurt review qualityIDE-based quality control is not enoughAI needs deterministic boundariesWhat enterprise quality gates should checkFormatting is only one source-code gateJava member ordering is harder than ...

The post Why Enterprise Java Teams Need Quality Gates Even More in the Age of AI appeared first on foojay.

]]>
Table of Contents
Enterprise quality is a scaling problemLocal differences become delivery problemsNoisy diffs hurt review qualityIDE-based quality control is not enoughAI needs deterministic boundariesWhat enterprise quality gates should checkFormatting is only one source-code gateJava member ordering is harder than it looksThe missing layer: JHarmonizerWhere it fits in the Java quality stackConclusion

Illustration of human developers and an AI assistant writing code together, with the code passing through an enterprise quality gate before reaching a trusted repository. People and AI can write code together, but enterprise repositories still need deterministic quality gates to protect code quality.

Enterprise quality is a scaling problem

Enterprise Java development is not only about writing correct code. It is about keeping a large, long-lived codebase understandable, reviewable and safe to change while many people and many tools touch it over time.

In a small project, informal discipline can be enough. A few developers agree on conventions, use similar IDE settings and fix inconsistencies during review.

That model breaks down in larger organizations. Teams change, ownership moves, modules outlive their original authors, and code is edited through different IDEs, web interfaces, scripts, generators and AI-assisted workflows.

This is where quality gates become important. They are not bureaucracy around the build. They are executable engineering agreements. If a rule matters for long-term maintainability, it should be runnable, repeatable and enforceable from the build pipeline.

Local differences become delivery problems

Most quality problems look small in isolation. One developer uses a different IDE profile. Another ignores a local inspection warning. Someone forgets to run tests. A dependency is updated without checking the broader impact. A generated change touches many files in a slightly different style.

The damage comes from accumulation. Similar modules stop following the same structure. Reviewers work harder to find the real behavior change. Static analysis findings arrive too late. Test coverage becomes uneven. Dependency rules drift. Build behavior becomes less predictable.

A large team therefore needs common rules that run the same way for everyone. Formatting, source structure, static analysis, dependency checks, test coverage and license checks should not depend on who made the change or which local setup happened to be configured correctly.

Readable and maintainable code is a delivery concern, not an aesthetic preference. The main consumer of source code is another developer: the person reviewing it today, debugging it in six months or extending it next year.

Noisy diffs hurt review quality

Weak automation often shows up as noisy pull requests. A developer changes a few lines of behavior, but the diff also contains reordered methods, import cleanup, blank-line changes and unrelated formatting noise.

The reviewer has to dig for the real change inside layout churn. That is tiring, slow and bad for review quality.

Good tooling separates these concerns. If a project has a canonical representation of source code, developers can bring files back to that representation before review. The diff becomes smaller, and the reviewer focuses on behavior instead of formatting archaeology.

IDE-based quality control is not enough

The natural answer is: let the IDE handle it. Modern IDEs are powerful productivity tools. IntelliJ IDEA, Eclipse and other environments can format Java code, optimize imports, rearrange class members, run inspections, show test coverage and integrate static-analysis plugins. For local work, that feedback is valuable. It helps developers produce cleaner code before they even run the build.

The problem starts when this local workflow becomes the quality strategy for a large distributed team. An IDE can help one developer on one machine. It cannot guarantee that every change in every branch was produced with the same editor, plugins, settings, imported profile and manual actions.

At enterprise scale, that assumption fails quickly. Developers may use IntelliJ IDEA, Eclipse, VS Code, terminal tools, repository web editors, generated code or automated migrations. Some remember the right action. Others do not. One workstation has the correct profile. Another has a slightly different setup.

IDE support remains useful, but repository protection must live somewhere independent of the developer's workstation. If a rule matters for the project, it should be part of the build, reproducible in Maven or Gradle, and enforceable in CI.

AI agents make this even more obvious. They do not reliably use your IDE, inspection profile, formatter settings, rearrangement rules or local quality plugins. Depending on IDE-based quality control becomes even weaker when not all code is produced through an IDE.

AI needs deterministic boundaries

AI does not remove the need for quality gates. It increases it.

AI agents can generate, refactor and explain code quickly. That is useful, but it also means more code can be produced with less friction by humans, scripts and AI assistants together. The repository needs stronger automatic boundaries around what is accepted.

The tempting mistake is to turn AI itself into the quality gate. For deterministic rules, that is the wrong default.

A prompt is not a quality gate. Asking an AI agent to check whether code follows a style guide, uses the right dependency policy, has correct formatting, or follows a source-structure convention is not the same as enforcing a rule. A model may follow the instruction, partially follow it, misunderstand it, or produce a different judgment when the context changes. That is not how enterprise gates should work.

If a quality rule can be expressed as a deterministic algorithm, it should be enforced by deterministic code. Formatting, import cleanup, dependency checks, static analysis, license checks and reproducible source ordering should be fast, cheap and repeatable. The same input should produce the same result. The same check should fail locally and in CI for the same reason.

AI can still be useful around this process. It can suggest fixes, explain a failed check, generate tests, or help a developer understand a static-analysis warning. But the final repository boundary should not depend on a model interpreting a prompt. It should depend on executable rules.

What enterprise quality gates should check

A quality gate is not one vague checkbox called "quality". In a serious Java project, it is a set of concrete checks that protect different parts of the delivery process.

In the best case, the build and CI pipeline should verify:

  • Build reproducibility: expected JDK version, Maven or Gradle version, plugin versions, compiler target, generated sources and repeatable build behavior.
  • Dependency governance: banned dependencies, dependency convergence, snapshot dependencies, duplicated versions, vulnerable libraries and license compatibility.
  • Compilation: main sources, test sources, annotation processors, generated code and selected Java language level.
  • Automated tests: unit tests, integration tests, contract tests, smoke tests and other project-specific test suites.
  • Coverage: minimum line or branch coverage, module-level thresholds and protection against silent coverage drops.
  • Static analysis: bug patterns, duplicated code, excessive complexity, risky APIs, nullability problems and maintainability rules.
  • Security and compliance: dependency vulnerability scanning, secret scanning, required license headers, SPDX metadata and internal repository rules.
  • Source-code policy: formatting, imports, line wrapping, naming conventions, generated-code exclusions, package rules and class structure.
  • Build output discipline: controlled warnings, stable reports, useful failure messages and artifacts that can be inspected after CI failure.

For enterprise development, these rules should be part of the build, not only part of local IDE setup. A common Maven or Gradle configuration gives the project one executable contract.

Locally, developers should have commands that can fix what is safe to fix automatically. For example, a Maven profile or plugin goal may format code, clean imports, reorder source structure, regenerate reports, or apply other safe mechanical changes.

In CI, the same project should have check-only execution. The pipeline should not silently rewrite code. It should verify that the code already follows the required rules. If formatting is wrong, imports are dirty, tests fail, coverage drops, dependencies violate policy, or source structure is inconsistent, the build should fail with a clear message.

This is how a written convention becomes an executable rule. Instead of repeating the same review comments again and again, such as "run the formatter", "fix imports", "update tests", "do not use this dependency", or "this class structure is inconsistent", the team moves these checks into the build pipeline. Reviewers can then focus on design, behavior, risk and business logic instead of acting as manual linters.

Formatting is only one source-code gate

Many quality gates are already common in mature Java projects. Build checks, tests, coverage thresholds, static analysis and dependency rules are familiar parts of CI pipelines.

Source-code policy is one part of that broader picture.

Formatting is the most familiar source-code gate. It controls the text-level shape of the file: spaces, indentation, wrapping, imports, blank lines and syntax layout. Java already has strong tools for this layer. google-java-format and palantir-java-format can make formatting reproducible, and they can be integrated into the build.

But source-code policy does not end at formatting. It does not decide where constants, fields, constructors, public methods, private helpers, accessors and nested types belong inside a class. That is a separate layer: source structure.

A file can be perfectly formatted and still be hard to scan because every class follows a different internal order. This is the gap between formatting and source restructuring.

Java member ordering is harder than it looks

Java declarations are not always independent. One constant may depend on another constant. A field initializer may depend on a field declared above it. Static or instance initialization blocks may rely on members that already exist earlier in the class.

For example, this order is safe:

private static final int DEFAULT_TIMEOUT_SECONDS = 30;
private static final int API_REQUEST_TIMEOUT_SECONDS = DEFAULT_TIMEOUT_SECONDS * 2;

Blind alphabetical sorting may accidentally produce this:

private static final int API_REQUEST_TIMEOUT_SECONDS = DEFAULT_TIMEOUT_SECONDS * 2;
private static final int DEFAULT_TIMEOUT_SECONDS = 30;

Now the change is not only cosmetic. API_REQUEST_TIMEOUT_SECONDS depends on DEFAULT_TIMEOUT_SECONDS, so the base constant must stay above the derived one.

A source restructuring tool therefore has to respect declaration-order dependencies. Similar issues can appear around field initializers, static initializers, instance initializers, enum constants, annotation values and other class-level declarations where order may matter.

The missing layer: JHarmonizer

This was the missing layer I could not find in the Java tooling ecosystem: a way to make Java class structure reproducible outside the IDE, enforceable from the build, and safe enough to respect declaration-order dependencies.

So I built JHarmonizer.

Before-and-after illustration showing JHarmonizer transforming a chaotic Java class layout into a predictable canonical order with dependency-safe structure and cleaner diffs. JHarmonizer reorganizes Java class members into a canonical structure, making code easier to scan, safer to review, and more consistent across teams.

JHarmonizer is an open-source Java source harmonization tool. It focuses on one layer of the quality workflow: making Java source structure and formatting reproducible from Maven, CLI and CI.

It can:

  • reorder Java class members while respecting declaration-order dependencies;
  • keep accessors together;
  • use different ordering strategies for interfaces, DTOs, tests, utility classes and regular production classes;
  • format the reordered result with Palantir Java Format;
  • run from Maven or the command line in auto-fix mode;
  • run in check mode as a CI quality gate.

Where it fits in the Java quality stack

JHarmonizer is not meant to replace the Java quality ecosystem. Static analyzers, bug detectors, coverage tools, architectural tests and code review all solve different problems.

A typical Java quality setup may include Maven Enforcer, PMD, CPD, SpotBugs, JaCoCo, license checks, sortpom and JHarmonizer. Each tool protects a different layer: dependencies, static checks, duplication, bug patterns, coverage, legal metadata, build files and source structure.

There is no single magic tool. Enterprise quality comes from boring, deterministic checks that protect different parts of the codebase.

Conclusion

AI will continue to change how code is produced. That is not a reason to weaken engineering discipline. It is a reason to automate more of it.

Large teams need rules that do not depend on local IDE settings, personal habits, memory or how an AI model interprets a prompt.

Formatting, static analysis, tests, coverage, dependency rules and license checks are already part of that picture. Java source structure deserves to be part of it too.

Readable and understandable code does not happen automatically in large teams. It has to be protected by process, tooling and automation.

That is what quality gates are really for: not slowing teams down, but helping them deliver safely, predictably and with less avoidable noise.

The post Why Enterprise Java Teams Need Quality Gates Even More in the Age of AI appeared first on foojay.

]]>
https://foojay.io/today/enterprise-java-quality-gates-ai/feed/ 0
Intro to the BoxLang Formatter https://foojay.io/today/intro-to-the-boxlang-formatter/ https://foojay.io/today/intro-to-the-boxlang-formatter/#respond Thu, 28 May 2026 17:45:09 +0000 https://foojay.io/?p=123985 Table of Contents Recommended Team Workflow You know the drill. Someone opens a PR and half the review comments are about tabs vs spaces, where braces go, or why that one function has its arguments formatted differently from everything else. ...

The post Intro to the BoxLang Formatter appeared first on foojay.

]]>
Table of Contents

You know the drill. Someone opens a PR and half the review comments are about tabs vs spaces, where braces go, or why that one function has its arguments formatted differently from everything else. It's noise. And it's over.

The BoxLang Formatter is here, and it handles all of that for you.

You can find the docs here: https://boxlang.ortusbooks.com/getting-started/ide-tooling/boxlang-formatter

What Is It?

The BoxLang Formatter is a built-in code formatting tool that ships with BoxLang. It enforces consistent style across .bx, .bxs, .bxm, .cfm, .cfc, and .cfs files — automatically.

It's not a linter. It doesn't just complain. It fixes your code, or tells CI to fail when style drift sneaks in.

Getting Started in 60 Seconds

If you have BoxLang installed, you already have the formatter. No extra install needed.

Format everything in your current directory:

boxlang format

That's it. It recurses through your project and rewrites supported files in place.

Want to target a specific path or file?

# A directory
boxlang format --source ./src

# A single file
boxlang format --source ./models/User.bx

Multiple paths at once (v1.14+):

boxlang format --source commands,models,services

Configure Your Style

The formatter works great out of the box with sensible defaults, but you can customize it with a .bxformat.json file in your project root.

Bootstrap one instantly:

boxlang format --initConfig

This drops a starter config in your current directory. From there, tweak what you care about. Here's a minimal example:

{
  "maxLineLength": 120,
  "tabIndent": true,
  "singleQuote": false,
  "braces": {
    "style": "same-line",
    "require_for_single_statement": true
  },
  "operators": {
    "comparison_style": "symbols"
  }
}

You've got control over indentation, line length, brace style, struct/array formatting, operator style, SQL keyword casing, import sorting, and a lot more. Only override what you need — everything else uses sensible defaults.

Lock It Down in CI

This is where it gets really useful. Run the formatter in check mode as a quality gate:

boxlang format --check --source ./
  • Exits 0 if everything is already formatted correctly
  • Exits non-zero if any file has drift

Drop that into your CI pipeline and pull requests with messy formatting simply won't merge. One command, no separate linter needed.

Recommended Team Workflow

  • Developers run boxlang format before pushing
  • CI runs boxlang format --check on every PR
  • PRs that fail must reformat before merge

No more style debates in code review. The formatter wins.

Format on Save in VS Code

If you want formatting to happen automatically as you work, the BoxLang LSP supports experimental format-on-save.

Step 1 - Enable it in .bxlint.json:

{
  "formatting": {
    "experimental": {
      "enabled": true
    }
  }
}

Step 2 - Add this to your VS Code settings.json:

{
  "[boxlang]": {
    "editor.formatOnSave": true
  },
  "[boxlang-template]": {
    "editor.formatOnSave": true
  }
}

Step 3 - Open the Command Palette and run:

  • BoxLang: Select BoxLang Version (pick latest)
  • BoxLang: Select LSP Version (pick latest)
  • Developer: Reload Window

Save a .bx file and it just formats. Local fast feedback, CI enforcement as the source of truth.

Coming from cfformat?

Already using cfformat in your project? Migration is a two-step process, and your existing style intent is preserved.

Step 1 - Convert your config:

boxlang format --convertConfig --source ./

This transforms your .cfformat.json into a .bxformat.json, keeping your rules intact.

Step 2 - Validate with check mode:

boxlang format --check --source ./

See what (if anything) drifted. Run the formatter once in a cleanup commit, then turn on --check in CI and you're done.

A Few Other Handy Options

Preview without rewriting files — pipe output to stdout instead:

boxlang format --overwrite false --source ./handlers/MainHandler.cfc

Exclude directories (v1.14+):

boxlang format --source . --excludes generated,vendor

Use a custom config path:

boxlang format --config ./config/.bxformat.json --source ./

The Bottom Line

Stop spending review cycles on style. The formatter handles it — in your editor, in your pre-commit hook, in CI. One command, consistent output, zero arguments about semicolons ever again.

Go format something:

boxlang format

Questions? Hit us up on Community & Support or open a discussion on the BoxLang repo. We'd love to hear how you're using it.

The post Intro to the BoxLang Formatter appeared first on foojay.

]]>
https://foojay.io/today/intro-to-the-boxlang-formatter/feed/ 0
A New Generation of Java Libraries Is Born: Wasm Becomes the Implementation Detail https://foojay.io/today/a-new-generation-of-java-libraries-is-born-wasm-becomes-the-implementation-detail/ https://foojay.io/today/a-new-generation-of-java-libraries-is-born-wasm-becomes-the-implementation-detail/#comments Tue, 26 May 2026 13:00:47 +0000 https://foojay.io/?p=123939 Table of Contents The problem every Java developer knowsWhat if the library just ran inside the JVM?The ecosystem: it's already hereEndive: a new chapter for WebAssembly on the JVMCompose, don't rewriteGet involved If you're running JRuby in production, you're running ...

The post A New Generation of Java Libraries Is Born: Wasm Becomes the Implementation Detail appeared first on foojay.

]]>
Table of Contents
The problem every Java developer knowsWhat if the library just ran inside the JVM?The ecosystem: it's already hereEndive: a new chapter for WebAssembly on the JVMCompose, don't rewriteGet involved

If you're running JRuby in production, you're running WebAssembly. If TrinoDB is evaluating your Python UDFs, that's WebAssembly too. If Microcks is running JavaScript dispatchers to route your mock API responses, WebAssembly is doing the work.

You didn't install a native binary. You didn't configure JNI. You didn't cross-compile anything for your target platform. It just works, because the Wasm module is hiding inside a regular JAR on your classpath.

This is the same pattern that already powers the web. Every day, millions of people use Google Sheets, Figma, or Photoshop without knowing that WebAssembly is what makes those applications possible. Wasm is already invisible infrastructure in the browser. Now the same thing is happening on the JVM.

The problem every Java developer knows

Some of the best libraries in the world are written in C, C++, Rust, or Go. SQLite. QuickJS. Protocol Buffers. OPA. They are battle-tested, widely deployed, and actively maintained. At some point, every Java project wants to use one of them.

The options have never been great. You can rewrite the library in Java, but that's expensive, error-prone, and perpetually behind the upstream. Or you can reach for JNI and ship platform-specific native binaries. That path comes with its own pain: your application becomes OS and architecture-specific. Execution leaves the safety and observability of the JVM. Distribution turns into a matrix of .so, .dylib, and .dll files. Debugging across the JNI boundary is nobody's idea of fun. And if you're in a restrictive environment, such as a locked-down container or a platform that blocks native library loading, JNI may not be an option at all.

For decades, this trade-off felt permanent. You could have the capability, or you could have the JVM's guarantees. Not both.

What if the library just ran inside the JVM?

WebAssembly changes this equation. Take a proven C or Rust library, compile it to Wasm, and run it within JVM boundaries. You keep everything the JVM gives you: guaranteed memory safety, fault isolation, platform independence, advanced JIT, observability, and the "write once, run anywhere" promise. The Wasm module becomes just another artifact inside your JAR, an implementation detail that your users never need to think about.

This isn't a theoretical possibility. It's happening right now, in production, across a growing ecosystem of Java libraries.

The ecosystem: it's already here

SQLite4j is a pure-Java JDBC driver for SQLite. The real SQLite, compiled to WebAssembly and embedded in a JAR. Before: you shipped platform-specific native binaries for every OS and architecture your users might run on, and hoped your CI matrix covered them all. After: one JAR, everywhere. Same SQLite, zero native dependencies. Add a Maven dependency and you have an embedded relational database.

QuickJs4j puts a full JavaScript runtime inside your Java application. QuickJS, a small, fast, standards-compliant JS engine, is compiled to Wasm and wrapped in a clean Java API. It's already powering real production systems. Microcks, a CNCF project, uses it for JavaScript dispatchers that control how mock responses are selected based on request headers, payloads, and parameters. It works across all Microcks distributions, including native-compiled images. Before: reach for Nashorn or Rhino (both deprecated), or GraalJS (heavy dependency). After: a lightweight, sandboxed JavaScript engine in a single dependency.

pglite4j embeds PGlite, a lightweight PostgreSQL build, inside the JVM via WebAssembly. In-process, no external server, no native binaries. Useful for testing, local development, or anywhere you need Postgres compatibility without running a separate process.

Protobuf4J compiles protocol buffers without the massive native protoc toolchain. Compile-time dependencies drop from 403MB to roughly 90MB. Same output. Fraction of the footprint.

JRuby uses Prism, the Ruby parser, compiled to Wasm and consumed from Java without JNI. The JRuby team didn't have to rewrite a parser or maintain platform-specific binaries. They added a dependency.

TrinoDB runs user-defined Python functions inside the query engine, sandboxed via WebAssembly. Users write Python UDFs; Trino executes them safely within JVM boundaries.

And the list keeps growing: Debezium single message transforms, OpenFeature flag evaluation, OPA policy engine running in-process (no network hop to an external sidecar), and more.

The pattern is always the same. Take a proven library. Compile it to Wasm. Wrap it in a JAR. Ship it on Maven Central. Your users add a dependency and get the capability. No JNI, no platform matrix, no native binary management. Wasm becomes the implementation detail.

Endive: a new chapter for WebAssembly on the JVM

This entire ecosystem was built on Chicory, a pure-Java WebAssembly runtime started in September 2023. Within two years it went from experiment to infrastructure, powering production systems across multiple organizations and proving that WebAssembly fits naturally into Java applications.

Now the project is entering its next chapter. Endive is a fork of Chicory and a Bytecode Alliance Hosted project, a vendor-neutral home where the project can grow openly. The community, the vision, and the code carry forward under the same Apache-2.0 license. What changes is that the project now belongs to its ecosystem.

Why the Bytecode Alliance? The BA is the foundation behind Wasmtime, WASI, and the Component Model: the core runtime, system interface, and component standard that define WebAssembly outside the browser. If Wasm is to become a durable cross-language component format, the JVM, one of the world's major managed runtime ecosystems, needs to be part of that story. Endive gives the BA community a place to collaborate on JVM integration directly.

The first major milestone is integrating the experimental Redline compiler into the mainline. Redline uses Cranelift, the same compiler backend behind Wasmtime, to compile Wasm to native machine code. On Java 25+, it achieves this with zero additional dependencies, thanks to Panama's Foreign Function & Memory API becoming a standard part of the platform. You get native-speed execution and the pure-Java packaging story.

Further out, the roadmap includes WasmGC, enabling the Java garbage collector to manage Wasm object references, and Component Model support for consuming cross-language components through typed interfaces.

For the full story on the fork, the governance model, and the migration path for existing Chicory users, see the Bytecode Alliance announcement.

Compose, don't rewrite

The future isn't about rewriting native libraries in Java. It isn't about wrestling with JNI or maintaining platform-specific builds. It's about composing your application from components written in any language (Rust, C, Go, Python, JavaScript) through WebAssembly.

One runtime. Any language. No rewrites.

Java has always been about writing code once and running it anywhere. WebAssembly extends that promise to any codebase in any language. With Endive, the JVM becomes a place where established libraries, regardless of what language they were written in, can run safely, portably, and at native speed.

Wasm becomes the invisible glue. The implementation detail that makes it all work, without you even noticing.

Get involved

The Endive repository is already available. The first release will prioritize strong continuity with Chicory, preserving compatibility and documenting migration steps clearly.

If you care about Java, WebAssembly, secure plugin systems, cross-language components, or the future of portable software, come build with us.

The post A New Generation of Java Libraries Is Born: Wasm Becomes the Implementation Detail appeared first on foojay.

]]>
https://foojay.io/today/a-new-generation-of-java-libraries-is-born-wasm-becomes-the-implementation-detail/feed/ 2
Introducing bx-jwt: Enterprise-Grade JSON Web Tokens for BoxLang https://foojay.io/today/introducing-bx-jwt-enterprise-grade-json-web-tokens-for-boxlang/ https://foojay.io/today/introducing-bx-jwt-enterprise-grade-json-web-tokens-for-boxlang/#comments Tue, 26 May 2026 10:14:00 +0000 https://foojay.io/?p=123950 Table of Contents The Fluent Builder — jwtNew()The BIF FunctionsHMAC Sign and VerifyRSA Sign and VerifyJWE Encryptionalg:none RejectionHMAC Minimum Key Lengths (RFC 7518 §3.2)Algorithm AllowlistClock Skew ToleranceAuthentication MiddlewareToken Refresh with Grace PeriodKid-Based Key RotationSigning (JWS)Encryption (JWE) JWT authentication is everywhere. ...

The post Introducing bx-jwt: Enterprise-Grade JSON Web Tokens for BoxLang appeared first on foojay.

]]>
Table of Contents

JWT authentication is everywhere. But rolling it correctly — with proper algorithm enforcement, key management, clock skew handling, JWE encryption, and zero security footguns — is anything but trivial. Today, we're shipping bx-jwt, a production-ready JWT/JWE module for BoxLang that handles all of it out of the box, so you can focus on building, not fighting cryptography.

bx-jwt is part of the BoxLang+ and BoxLang++ subscription tiers — our enterprise-grade module collection built for teams that take security seriously.

What is bx-jwt?

bx-jwt is a full implementation of the JWT/JWE specification stack for BoxLang:

  • JWS (JSON Web Signature) — HMAC, RSA, and Elliptic Curve signing
  • JWE (JSON Web Encryption) — RSA and symmetric encryption
  • RFC 7518 — JSON Web Algorithms
  • RFC 7519 — JSON Web Token

It ships with two APIs that serve different tastes: a fluent builder for expressive, chainable token construction, and a suite of BIF functions for direct, functional-style usage. Both share the same engine, key registry, and security model.

Two APIs, One Module

The Fluent Builder — jwtNew()

When readability matters, the fluent builder gives you a clean, chainable surface for token construction. Call jwtNew() and chain your claims. Terminate with .sign() or .encrypt().

token = jwtNew()
    .subject( "user-123" )
    .issuer( "auth-service" )
    .audience( "mobile-client" )
    .claim( "roles", [ "admin", "user" ] )
    .expireIn( 3600 )
    .header( "kid", "v1" )
    .sign( secret, "HS256" );

Every standard claim has a named method. Custom claims go through .claim( key, val ). Headers via .header( key, val ). Swap .sign() for .encrypt() and you have a JWE. It reads like what it does. 🎯

The BIF Functions

For teams that prefer a direct, functional style, all operations are available as first-class BoxLang BIFs:

BIF Purpose
jwtCreate() Sign a payload struct into a compact JWS token
jwtVerify() Verify signature and validate claims — throws on failure
jwtValidate() Like jwtVerify() but returns true/false
jwtDecode() Inspect header/payload without signature verification
jwtRefresh() Re-issue a token with fresh iat, jti, and optional new exp
jwtEncrypt() Encrypt a payload as a compact JWE token
jwtDecrypt() Decrypt a JWE token and return claims
jwtGenerateSecret() Cryptographically random HMAC secret (Base64-encoded)
jwtGenerateKeyPair() RSA or EC key pair as PEM strings

Get Started in Seconds

HMAC Sign and Verify

secret  = jwtGenerateSecret( 256 );
token   = jwtCreate( { sub: "user-123", iss: "my-api", roles: [ "admin" ] }, secret, "HS256" );
payload = jwtVerify( token, secret, "HS256" );
writeOutput( payload.sub ); // user-123

RSA Sign and Verify

keys    = jwtGenerateKeyPair( "RS256" );
token   = jwtCreate( { sub: "user-123" }, keys.privateKey, "RS256" );
payload = jwtVerify( token, keys.publicKey, "RS256" );

JWE Encryption

Sensitive payloads — PII, PHI, internal claims that must stay opaque — belong in a JWE, not a JWS. bx-jwt handles both:

token   = jwtEncrypt(
    { sub: "patient-456", phi: { dob: "1990-01-15" } },
    secret32bytes,
    { keyAlgorithm: "dir", encAlgorithm: "A256GCM" }
);
payload = jwtDecrypt( token, secret32bytes, { keyAlgorithm: "dir", encAlgorithm: "A256GCM" } );

Or nest them — sign first, encrypt the signed token — for the full sign-then-encrypt pattern:

// Inner signed JWT
signedToken = jwtCreate( { sub: "u1", role: "admin" }, innerPrivKey, "RS256", {
    headers: { cty: "JWT" }
} );

// Outer encrypted JWE
encryptedToken = jwtEncrypt( signedToken, outerPubKey, {
    keyAlgorithm : "RSA-OAEP-256",
    encAlgorithm : "A256GCM"
} );

Enterprise Key Management with the Key Registry

This is where bx-jwt separates from basic JWT libraries. The Key Registry lets you define named keys once in configuration and reference them by name throughout your entire application. Keys never appear in application logic. Rotation is a config change, not a code change.

// ModuleConfig.bx
settings = {
    keys: {
        "api-signing": {
            algorithm : "HS256",
            secret    : "${Setting: env.JWT_HMAC_SECRET not found}"   // env var substitution built-in
        },
        "api-rsa": {
            algorithm  : "RS256",
            privateKey : "/etc/keys/api-private.pem",
            publicKey  : "/etc/keys/api-public.pem"
        },
        "partner-public": {
            algorithm : "RS256",
            publicKey : "/etc/keys/partner-public.pem"  // verify-only key
        }
    },
    defaultSigningKey : "api-signing",
    defaultVerifyKey  : "api-signing",
    defaultAlgorithm  : "HS256",
    defaultIssuer     : "my-api",
    defaultAudience   : "web",
    defaultExpiration : 3600,
    generateIat       : true,
    generateJti       : true
}

With defaults fully configured, the key and algorithm arguments become optional everywhere:

// No key argument, no algorithm argument — resolved from registry
token   = jwtCreate( { sub: "user-123" } );
payload = jwtVerify( token );

Keys can also be registered at runtime via the JWTService:

jwtService = getBoxContext().getRuntime().getGlobalService( "JWTService" );
jwtService.registerKey( "session-key", { algorithm: "HS256", secret: generateSecureKey() } );

Security by Default — Not by Configuration 🛡️

bx-jwt is built with the attack surface in mind. Security properties are unconditional — they cannot be turned off:

alg:none Rejection

The classic JWT attack. bx-jwt unconditionally rejects tokens with alg:none. Passing an unsigned token to jwtVerify() or jwtRefresh() always throws JWTVerificationException. No configuration switch, no override. It simply doesn't work.

HMAC Minimum Key Lengths (RFC 7518 §3.2)

Short HMAC secrets are a real-world vulnerability. bx-jwt enforces RFC 7518 minimums:

Algorithm Minimum Key Length
HS256 32 bytes (256 bits)
HS384 48 bytes (384 bits)
HS512 64 bytes (512 bits)

Use jwtGenerateSecret( bits ) and you're always compliant.

Algorithm Allowlist

Algorithm-confusion attacks exploit servers that accept any algorithm the token header declares. Lock your application to a known set:

// Only HS256 and RS256 are accepted — anything else throws
allowedAlgorithms: [ "HS256", "RS256" ]

Clock Skew Tolerance

Distributed systems have clock drift. bx-jwt ships with a configurable clockSkew (default: 60 seconds) that prevents legitimate tokens from failing exp/nbf validation due to minor time differences between services. Tune it per environment:

// Strict environment
payload = jwtVerify( token, secret, "HS256", { clockSkew: 0 } );

// Distributed system with known drift
payload = jwtVerify( token, secret, "HS256", { clockSkew: 120 } );

Real-World Patterns

Authentication Middleware

function requireAuth() {
    var authHeader = getHttpRequestData().headers[ "Authorization" ] ?: ""
    if ( !authHeader.startsWith( "Bearer " ) ) {
        bx:header statusCode=401;
        abort;
    }

    var token = authHeader.removeFirst( "Bearer " )

    if ( !jwtValidate( token, application.jwtSecret, "HS256" ) ) {
        bx:header statusCode=401;
        abort;
    }

    request.currentUser = jwtVerify( token, application.jwtSecret, "HS256", {
        claims: { iss: "auth-service", aud: "api" }
    } );
}

Token Refresh with Grace Period

function refreshToken( token ) {
    try {
        return jwtRefresh( token, application.jwtSecret, "HS256", {
            allowExpired : true,   // honor recently expired tokens
            expireIn     : 3600,
            claims       : { iss: "auth-service" }
        } );
    } catch ( "bxjwt.JWTVerificationException" e ) {
        // Bad signature — not refreshable
        return "";
    }
}

Kid-Based Key Rotation

function verifyWithKeyRotation( token ) {
    var decoded = jwtDecode( token );
    var kid     = decoded.header.kid ?: "default";
    var key     = getKeyForKid( kid );
    return jwtVerify( token, key, decoded.header.alg );
}

Full Algorithm Support

Signing (JWS)

Algorithm Type Notes
HS256, HS384, HS512 HMAC Symmetric
RS256, RS384, RS512 RSA Asymmetric — private signs, public verifies
ES256, ES384, ES512 Elliptic Curve Smaller keys than RSA, equivalent security

Encryption (JWE)

Key Algorithm Content Encryption Key Type
RSA-OAEP-256 A256GCM RSA key pair
dir A256GCM 256-bit symmetric secret

Installation

# CommandBox
box install bx-jwt

# BoxLang CLI
install-bx-module bx-jwt

bx-jwt requires a BoxLang+ or BoxLang++ subscription. 🔑

This module ships as part of our enterprise module collection — a growing library of production-ready, security-focused, professionally maintained modules available exclusively to BoxLang+ subscribers.

BoxLang+/++/Starter

bx-jwt is one of many enterprise modules available under BoxLang+/++/Starter. When you subscribe, you get:

  • 🔐 bx-jwt and the full enterprise module library
  • ⚡ Priority support from the Ortus team
  • 🏗️ Access to upcoming enterprise modules as they ship
  • ❤️ You fund the continued development of BoxLang as a community-supported open source project
    View Plans & Subscribe → boxlang.io/plans

Resources

JSON Web Tokens are a solved problem. Now BoxLang has the enterprise solution to prove it. Install bx-jwt, protect your applications, and ship with confidence. 🚀

The post Introducing bx-jwt: Enterprise-Grade JSON Web Tokens for BoxLang appeared first on foojay.

]]>
https://foojay.io/today/introducing-bx-jwt-enterprise-grade-json-web-tokens-for-boxlang/feed/ 1
Context Is a Budget — Eight levers and three workflow patterns https://foojay.io/today/context-is-a-budget-eight-levers-and-three-workflow-patterns/ https://foojay.io/today/context-is-a-budget-eight-levers-and-three-workflow-patterns/#comments Fri, 22 May 2026 12:52:06 +0000 https://foojay.io/?p=123791 Table of Contents Where the tokens actually goThe Eight LeversA. Context engineering — scope your asksB. Prompt caching — order mattersC. Tool & MCP hygiene — every schema is a taxD. Custom instructions & skills — codify it onceE. Model ...

The post Context Is a Budget — Eight levers and three workflow patterns appeared first on foojay.

]]>
Table of Contents
Where the tokens actually goThe Eight LeversThree workflow patterns that compoundThe Monday checklistClosing

Eight levers and three workflow patterns that pay for themselves in a week.

A team of fifty developers can quietly burn $30,000 a month on AI coding assistants without anyone noticing. Premium-request quotas vanish by the third week. The bill arrives. Nobody has a story for where it went.

The cost is the obvious pain. The other two are sneakier:

  • Latency. Bigger contexts take longer. The model thinks more, but you also wait more.
  • Context rot. This is the surprising one. Anthropic and Chroma have both shown that as the context window fills up, model recall and reasoning degrade — even well inside the advertised window. The 200K-token model is genuinely worse at the 150K mark than at the 20K mark. More context is not free; past a point, it's actively harmful.

The mental model that fixes all three: stop treating context as a free buffet. Treat it as a budget you spend on every turn.

This post is a practical guide to spending it well: where the tokens actually go, eight levers that move the needle, and three workflow patterns that compound on top of them.

Token distribution by bucket — most teams are surprised which one dominates

Where the tokens actually go

Every request to a coding assistant is a stack of buckets. The shape varies by tool and session, but it tends to look like this:

Bucket Typical share Notes
System prompt / instructions 5–15% Boilerplate that's been copy-pasted for months
Tool / function schemas 10–40% Re-sent on every turn
Retrieved files & code chunks 20–60% The biggest lever, almost always
Conversation history 10–30% Grows linearly until you compact it
Model output 5–20% Verbose prose is expensive to produce and to read

A few things to notice:

  • Tool schemas dominate more than people expect. Five connected MCP servers can easily contribute 5,000–10,000 tokens to every request before you've typed a word. The model doesn't have to use the tool — the schema ships either way.
  • Conversation history grows without bound. A 30-turn chat is paying for the first 29 turns on every new question, plus your fresh one.
  • Output is small in volume but expensive per token. On most direct APIs, output tokens cost three to five times input tokens. A reply that says "Sure! Let me explain what I'm about to do…" before doing it is pure tax.

Rule of thumb: profile your own traffic before optimizing. The bucket dominating your sessions is rarely the one your gut says.

In a Copilot context, you can't see token counts directly — but you can see the symptom. Open Output → "GitHub Copilot Chat" and watch the ccreq lines: each one shows the model, latency, and request type per turn. When the same question takes three times longer in chat #2 than chat #1, you've just watched your token meter the entire time.

VS Code Output panel showing Copilot Chat ccreq lines — your free token meter

The Eight Levers

These aren't in priority order — they're in the order you'd naturally encounter them in a session. The first three (context, caching, tools) are about the request shape. The next three (instructions, model, output) are about how you talk to the assistant. The last two (repo, observability) are the foundations that make all of the others stick.

Eight levers that mananges token budget


A. Context engineering — scope your asks

The single biggest waste in most AI workflows is asking vague questions of agent-mode chat with full codebase access. The agent dutifully explores, reads ten files to find the two it needed, summarizes them all, and then answers. You pay for every step.

Compare:

Bad: "Refactor the order confirmation email to use the new template engine."
The agent opens four files under src/main/java/com/example/demo/email/, reads WelcomeEmailService.java for context it doesn't need, considers whether a templates/ resource directory should exist, and proposes a sprawling diff that renames a method on the way through.

Good: "Refactor #file:src/main/java/com/example/demo/email/OrderConfirmationService.java to call render on #file:src/main/java/com/example/demo/email/TemplateEngine.java instead of renderLegacy. Keep behaviour identical."
The agent opens two files. The diff is three semantically meaningful lines. The whole turn is roughly a tenth of the cost.

Specificity is free. Every #file: (Copilot) or explicit path (Claude Code) you provide is a chunk the agent doesn't have to find. Every "keep behaviour identical" is a sentence of guard-rails that prevents a 200-line side quest.

Do this Monday: make #file: your default. Use agent-mode-with-broad-retrieval only when you genuinely don't know what you don't know.


B. Prompt caching — order matters

Every major provider supports prompt caching now. Anthropic and OpenAI both charge roughly 10% of base input cost for cache hits. Google's Gemini does it explicitly. The mechanism is the same: a stable prefix at the front of your prompt is cached after the first request and read back cheaply on subsequent ones.

The cost discipline is therefore about order:

[ tool definitions ]    ← rarely change         ┐
[ system prompt ]       ← rarely changes      │ cache these
[ skills / rules ]      ← stable per repo           ┘
[ retrieved files ]     ← changes per task
[ conversation ]        ← changes every turn

Static at the top, dynamic at the bottom. The longest stable prefix you can construct is the most cacheable one.

The classic anti-pattern is innocent-looking and brutal is to have dynamic values/variables part of your instructions, custom agent files. It will most likely busts the cache on every request. You will pay full price for the same 10 KB of preamble all day. The fix is to push dynamic content down into the user message or tail of the prompt.

Do this Monday: audit the first 200 tokens of your system prompts. Anything that changes per-request belongs further down.


C. Tool & MCP hygiene — every schema is a tax

Each connected tool ships its full JSON schema with every request. A typical MCP server with 8–15 tools costs 400–2,500 tokens per turn. Five servers connected? You may be paying 5,000–10,000 tokens per turn for tool definitions the model never invokes.

Treat MCP servers like browser extensions: useful, but only the ones you actually need today.

// .vscode/mcp.json — keep this short
{
  "servers": {
    "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem"] }
    // disable github, playwright, brave-search, etc. when you don't need them
  }
}

The same discipline applies to the tools you build yourself. A tool that returns { id, summary } is cheap. A tool that returns a 50-field JSON object is expensive — the model re-processes all 50 fields on every turn it's referenced. Default to compact responses with optional ?expand=... for the rare caller that needs the rest.

Do this Monday: open MCP server list, disable everything you didn't actively use this week. Re-enable on demand.


D. Custom instructions & skills — codify it once

Anything you find yourself re-typing in chats belongs in an instructions file. The exact filename varies — .github/copilot-instructions.md, CLAUDE.md, AGENTS.md, Cursor Rules — but the principle is identical: write your team conventions once, commit them, and let every chat in the repo inherit them.

A small example is worth more than a long one:

A 6-line copilot-instructions.md is enough to change every session in the repo

Six lines. Now no chat in this repo will propose Jest, no chat will dump a whole-file rewrite when a diff would do, and no chat will preface its answer with "Sure! Let me explain what I'm about to do…"

For stack-specific rules, use path-scoped instructions. In Copilot:

---
applyTo: "src/main/java/**/*.java"
---
# Test conventions for src/
- Use JUnit 5 via `mvn test`.
- Tests mirror the source tree under `src/test/java/...` as `<Name>Test.java`.

This file is loaded only when a matching file is in scope. Repo-wide rules go in the global instructions; stack-specific rules go in scoped ones. Both are committed, both are versioned, both are team artifacts — not personal preferences buried in someone's IDE settings.

Do this Monday: check what you've typed into chat windows in the last week. Anything that reappeared more than twice is a candidate for an instructions file.


E. Model routing — start cheap, escalate when stuck

Routine tasks pick the most expensive model by default if you let them. You probably just paid 10× for the same answer.

A defensible default routing table:

Task Model Multiplier (Copilot)
Inline completions, simple chat Cheapest available (e.g. GPT-4.1)
Real coding work Mid-tier (GPT-5 / Claude Sonnet)
Long-context refactor / agent mode Mid-tier with long context
Genuinely hard reasoning Top-tier (Claude Opus) 10×

The rule is: start cheap, escalate only when stuck. "Stuck" means you've tried the mid-tier model with good context and it's plainly missing the point — not "I want to feel sure," not "I have time to spare."

The math compounds. A team of fifty doing twenty agent runs each per day at 10× costs five times more than at 2× — for the same diffs, on most days.

Do this Monday: pin your default to the mid-tier. Make Opus a deliberate choice with a reason.


F. Output discipline — diffs, not novels

Every model has a "let me explain what I'm about to do" reflex. It's polite. It's also pure cost.

Same fix, two ways to ask:

"In templateEngine.js, the welcome template is missing an exclamation mark. Show me the updated file."
→ 30 lines back. (With a 600-line file, 600 lines.)

"In templateEngine.js, the welcome template is missing an exclamation mark. Reply with a unified diff only, no commentary."
→ 5 lines back.

Output tokens are typically three to five times the price of input tokens on direct APIs. In a per-request model like Copilot's, verbose output still hurts: it increases latency, fills the context for subsequent turns, and evicts earlier useful content sooner.

The leverage is in the system prompt. Two lines in copilot-instructions.md make every chat in the repo behave better forever:

- Be concise. No preamble.
- Prefer diffs over full files.

Do this Monday: add those two lines.


G. Repo hygiene — what the indexer sees

The indexer that powers retrieval respects .gitignore. Tighten it.

+target/
+*.class
+*.jar
+.idea/
+*.iml
 *.log

Important gotcha: if a file is already tracked in git, adding the path to .gitignore does not untrack it — the indexer still sees it. You also need:

git rm --cached target/demo-0.0.1-SNAPSHOT.jar

For secrets, fixtures, and vendored deps, use content exclusions at the org/repo level (most coding-assistant providers expose this).

The other half of repo hygiene is summary comments at the top of each module:

// TemplateEngine — central renderer. Use render(id, data) for new emails.
// renderLegacy(id, data) is deprecated and only used by OrderConfirmationService.
// Templates registered: welcome, order_confirmation_v2.

Three lines, ~50 tokens. Now "what does the template engine do?" can be answered without reading the rest of the file. A 200-token summary at the top of each module beats re-reading 5,000 tokens of code, every single time.

Do this Monday: git rm --cached whatever shouldn't be indexed; add three-line summaries to your top-of-mind modules.


H. Observability — latency is your token meter

You can't see Copilot's token counts. You don't need to. Use the proxy you already have:

Reply latency ≈ Input tokens
< 5 s 20 s Near limit — start a new chat

When the same question takes three times longer in your fourth chat than in a fresh one, you've just watched your context bloat in real time. The fix is "new chat with a summary," not "wait it out."

You can also lint for context bloat the same way you lint for bundle size. A 30-line script in CI is enough to catch the most common regressions:

// fail if any .github/instructions/*.md exceeds 150 lines
import { readdir, readFile } from "node:fs/promises";
const files = (await readdir(".github/instructions")).filter(f => f.endsWith(".md"));
let failed = false;
for (const f of files) {
  const lines = (await readFile(`.github/instructions/${f}`, "utf8")).split("\n").length;
  console.log(`${lines > 150 ? "❌" : "✅"} ${f}: ${lines} lines`);
  if (lines > 150) failed = true;
}
if (failed) process.exit(1);

Wire it into CI and context bloat stops accumulating silently across PRs.

A 30-line lint script catches context bloat the way bundle-size lints catch JS bloat

Do this Monday: put a stopwatch next to your editor for one day. Count "Amsterdam" (not Mississippi's) . You'll know which chats to rotate.

Three workflow patterns that compound

The eight levers above shrink the cost of an individual turn. These three patterns shrink the number of expensive turns. Apply them on top.

3 workflow patterns


1. The Ralph Wiggum loop

Named after the Simpsons character whose superpower is relentless dumbness. The recipe is unglamorous on purpose:

  1. Write a TODO.md with checkbox tasks.
  2. Open agent-mode chat with a cheap model.
  3. Tell it: "Read TODO.md. Pick the first unchecked item. Implement only that. Run npm test. If green, check the box and commit. Pick the next. Repeat."

That's it. The agent burns through the list one item at a time.

Why it works:

  • Each iteration starts with a small, fresh context. The chat history isn't growing the way it would in a free-form conversation.
  • State lives on disk (TODO.md and git commits), not in conversation tokens.
  • A cheap model is good enough, because each task is small and self-contained.
  • It's restartable. Kill the chat halfway, start a new one, run the prompt again — it picks up where it left off.

After it runs, git log --oneline reads like a changelog: one commit per task, message starts with the task title, easy to revert any one step. Compare with the typical "fix things" mega-commit and you'll never go back.

TODO.md mid-loop on the left, the per-task git log on the right — disk is the memory


2. Auto-compact

Most assistants don't compact aggressively on their own. You have to drive it.

When a chat hits 60–80% of the context window (you'll know — replies start to crawl), stop and ask:

Summarize what we've discussed: the goal, files we've touched, decisions made, open questions, and the next step. Keep it under 300 words and use bullet points.

Save the output to plan.md. Open a brand new chat. Attach it:

Continue from #file:plan.md. The next step is…

The new chat's first request is dramatically smaller than the old chat's last one. The model picks up the thread without missing a beat. Roughly: a 4 KB summary keeps 95% of the signal at 3% of the cost.

The bonus pattern: that summary file becomes a stable, cacheable prefix. Every future chat that references it benefits from prompt caching on top of the compaction. Two compounding wins for one summarization.

If you are interested in a sofisticated implementation of compaction, check this skill which is used by some of the custom agents.

Rule of thumb: one task per chat. New task → new chat with summary attached.


3. Planner → Implementer → Reviewer (agent handover)

This is the one that changes how features get built. Three short, focused chats with three different model choices and one shared artifact:

The handover artifact is the only thing that crosses the boundary — never chat history

  • Plannerexpensive model, one call. Reads the feature request, produces plan.md with goal, acceptance criteria, tasks, files expected to change, out-of-scope items, and risks. No code yet.
  • Implementercheap model, agent mode, fresh chat. Sees only plan.md. Runs a Ralph loop on it: pick first unchecked task, implement, test, check the box, commit, repeat.
  • Reviewerexpensive model, fresh chat. Sees only plan.md and the diff. Marks each acceptance criterion PASS or FAIL, lists bugs, smells, out-of-scope edits. Ends with VERDICT: APPROVE or VERDICT: REQUEST CHANGES.

Three chats, ~5–8 premium requests total for an end-to-end feature. Compare with one mega-chat using the most expensive model the whole way: easily 30+ requests at 10× the multiplier.

The crucial discipline: the handover artifact (plan.md, the diff, the review notes) is the only thing that crosses the boundary. Never chat history. That's how you keep each agent's context small, focused, and cheap.

The Monday checklist

Pin this to your team's wiki. Take what's useful, ignore the rest.

Repo setup

  • [ ] Add a top-level instructions file (copilot-instructions.md, AGENTS.md, CLAUDE.md, or your tool's equivalent) with build, test, lint, conventions, and output-style rules.
  • [ ] Add path-scoped instruction files for stack-specific rules (e.g. test conventions under src/).
  • [ ] .gitignore build outputs, snapshots, and large fixtures. git rm --cached anything already tracked.
  • [ ] Add three-line "what does this module do" summary comments to your top 10 modules.
  • [ ] Add a CI lint that fails if instruction files exceed ~150 lines or prompt files exceed ~250 lines.

Per-session habits

  • [ ] Disable MCP servers you don't need this session. Re-enable on demand.
  • [ ] Default to a mid-tier model. Escalate to a top-tier model only when stuck — and only with a reason.
  • [ ] Use #file: (or your tool's equivalent) instead of broad-retrieval / agent mode for scoped tasks.
  • [ ] Ask for diffs, not full files.
  • [ ] Start each new task in a fresh chat.
  • [ ] When responses start to crawl (~60% context), summarize to a plan.md and continue in a new chat.

Workflow patterns to try this week

  • [ ] Run a Ralph loop on a TODO.md of small chores.
  • [ ] Use the planner / implementer / reviewer split for one real feature. Notice the request count.
  • [ ] Treat latency as your token meter. Count Amsterdam for one day.

Closing

The mindset shift is small and the wins are not.

Prompt engineering used to be about clever phrasing. Context engineering — what this post was really about — is about what's in the window and what isn't. Smaller prompts, fewer tools, scoped retrieval, summaries instead of histories, cheap models for cheap work, expensive models for the rare hard parts.

None of it is novel. None of it is hard. Most teams don't actually have a token problem; they have a discipline problem. The levers are boring. The compounding is real: a team that adopts even half of the above will see latencies fall, premium-request burn drop noticeably, and, counterintuitively, answer quality go up, because the model isn't drowning in irrelevant context.

One sticky line to take with you:

The worst tokens are the ones you're paying for and not noticing.

Watch your ccreq lines. Count Amsterdams. Spend the budget like it's yours.

The post Context Is a Budget — Eight levers and three workflow patterns appeared first on foojay.

]]>
https://foojay.io/today/context-is-a-budget-eight-levers-and-three-workflow-patterns/feed/ 2
Introducing skills.boxlang.io — The Open Agent Skills Ecosystem for BoxLang & the Ortus World https://foojay.io/today/introducing-skills-boxlang-io-the-open-agent-skills-ecosystem-for-boxlang-the-ortus-world/ https://foojay.io/today/introducing-skills-boxlang-io-the-open-agent-skills-ecosystem-for-boxlang-the-ortus-world/#respond Thu, 21 May 2026 11:42:26 +0000 https://foojay.io/?p=123899 Table of Contents 🤔 The Problem: AI Knowledge Doesn't Scale by Copy-Paste🎓 What Is a Skill?📥 Install in Seconds: Two Paths, One Standard⚡ Option 1 — npx skills (works everywhere)🥊 Option 2 — ColdBox CLI (deep BoxLang/ColdBox integration)🔷 Core Repositories ...

The post Introducing skills.boxlang.io — The Open Agent Skills Ecosystem for BoxLang & the Ortus World appeared first on foojay.

]]>
Table of Contents
🤔 The Problem: AI Knowledge Doesn't Scale by Copy-Paste🎓 What Is a Skill?📥 Install in Seconds: Two Paths, One Standard🔷 Core Repositories — Curated by Ortus⭐ A Taste of What's Available🌐 Submit Your Own — Community Skills, Security First🛠 How Your Agent Actually Uses It🔮 Why This Matters Beyond BoxLang🎯 Get Started Now📚 Resources

Today we're launching something we've been quietly building for months: skills.boxlang.io — a public, agent-agnostic directory for AI skills covering BoxLang, ColdBox, TestBox, CommandBox, and the entire Ortus ecosystem.

If you've ever pasted a 400-line system prompt into yet another AI agent, watched two of your bots drift onto subtly different versions of the same coding standard, or spent half a Friday afternoon trying to convince an LLM that BoxLang is not Java and is not CFML, or how to code for Modern CFML; this launch is for you. 🎯

The numbers at launch:

  • 203+ curated skills available on day one
  • 8,000+ installs already, before public announcement
  • 3 core repositories maintained directly by Ortus Solutions
  • Multiple agents supported — Claude Code, Cursor, GitHub Copilot, Codex, OpenCode, and more
    Let's dig into what it is, why we built it, and how to start using it in the next 30 seconds. 🚀

🤔 The Problem: AI Knowledge Doesn't Scale by Copy-Paste

Every team building with AI agents eventually hits the same wall.

You write a great system prompt that teaches an agent your SQL conventions. Then a teammate spins up a new bot and pastes a slightly older version. A month later there's a third variant in a Slack snippet that nobody can find. Your "single source of truth" is now three sources of conflict, and the agent's outputs reflect every one of them.

This isn't a discipline problem — it's an architecture problem. System prompts are plain strings, and plain strings don't have a source of truth. They aren't versioned, aren't audited, aren't shared, and aren't discoverable.

Anthropic's Agent Skills open standard — Markdown files with frontmatter metadata, distributed as SKILL.md — gave the industry a real answer. BoxLang AI 3.0 implemented it natively. And now skills.boxlang.io brings the missing piece: a public, curated, security-audited registry where these skills live, are versioned, and can be installed into any AI agent in seconds. 💚

🎓 What Is a Skill?

A skill is a portable, reusable unit of expertise — a SQL coding style guide, a tone-of-voice policy, a ColdBox conventions cheat sheet, an API design standard, a security ruleset. Anything your AI assistant should know before it starts answering.

Each skill is a Markdown file (SKILL.md) with optional YAML frontmatter:

---
description: Use this skill when writing, reviewing, or formatting any
  Ortus Solutions code (BoxLang, CFML, or Java) to ensure it follows
  the official Ortus coding standards.
tags: [boxlang, cfml, java, coding-standards, ortus]
---

# Ortus Coding Standards

Always use spacing inside parentheses and brackets for readability.
Prefer closures with `=>` over anonymous functions.
Use lambdas with `->` when no external scope is needed.
...

Define it once. Inject it everywhere. Let your codebase — not your clipboard — be the source of truth. 📚

📥 Install in Seconds: Two Paths, One Standard

We built skills.boxlang.io to be agent-agnostic. Whatever AI tool your team prefers, the skills work the same way. You have two install paths.

⚡ Option 1 — npx skills (works everywhere)

Powered by skills.sh, an open-source, agent-agnostic CLI for discovering, installing, and managing SKILL.md files across Claude Code, GitHub Copilot, Cursor, Codex, and more. It reads the BoxLang Skills Hub catalog, security-audits community content, and drops files into the correct agent directory in one command.

# Install an entire repository of skills
npx skills add ortus-boxlang/skills

# Or grab a single, focused skill
npx skills add ortus-boxlang/skills/coldbox-basics

No global install needed. Works with any Node.js. 🌐

🥊 Option 2 — ColdBox CLI (deep BoxLang/ColdBox integration)

If you're already living in the ColdBox world, the ColdBox CLI 8.11 release wires the directory directly into your project workflow:

# Browse the directory interactively
coldbox ai skills install --list

# Filter by source or category
coldbox ai skills install --list coldbox/skills
coldbox ai skills install --list coldbox/skills/coldbox-testing

# Install a specific skill
coldbox ai skills install ortus-boxlang/skills/async-programming

# Search the registry
coldbox ai skills find "rest api"

Bonus: when you box install a module that has skills published to the directory, coldbox ai refresh auto-installs them. Skills become infrastructure, not setup. 💚

🔷 Core Repositories — Curated by Ortus

Three core repositories are officially maintained by Ortus Solutions. Skills here are trusted by default and skip the community audit step.

Repository Focus
ortus-boxlang/skills BoxLang language, runtime, BIFs, and core modules
coldbox/skills ColdBox MVC framework patterns and conventions
ortus-solutions/skills WireBox, TestBox, LogBox, and the broader Ortus module library

Want a skill added to a core repo? Open a pull request. Add your SKILL.md inside a new folder, include valid YAML frontmatter, and the Ortus team will review and merge it. Once merged, it's automatically imported the next time the hub syncs. ⚡

⭐ A Taste of What's Available

A small sample of skills you'll find in the directory at launch:

  • code-documenter — Producing or improving developer-facing documentation for codebases, APIs, modules, and architecture decisions
  • ortus-java-coding-standards — Official Ortus formatting and structural conventions for BoxLang, CFML, and Java
  • javascript-expert — Modern JavaScript correctness, async flows, module design, and architectural refactors
  • alpinejs-expert — Alpine.js component state, directives, transitions, and reusable stores
  • vite-expert — Vite-based frontend builds, HMR diagnostics, plugin customization, and Vitest integration
  • vuejs-expert — Composition API patterns, routing, forms, testing, and SSR-aware component design
  • async-programming — BoxLang futures, parallel execution, and concurrency primitives
  • coldbox-basics — ColdBox MVC conventions, handlers, models, interceptors, and module architecture
    …and 195+ more. Browse the full directory at skills.boxlang.io/skills. 🎯

🌐 Submit Your Own — Community Skills, Security First

Don't want to contribute to a core repo? Publish your own GitHub repository as a Community source or send us a Pull Request to any of our repos. Community skills are listed alongside core skills in the directory and go through automated security auditing before being made available, so consumers can install them with confidence.

The submission flow is straightforward:

  • Create a GitHub repository with one or more SKILL.md files, each in its own subfolder (e.g. my-skill/SKILL.md)
  • Add YAML frontmatter with at minimum name, description, and tags
  • Write clear, accurate documentation in the Markdown body
  • Submit your repo and we'll review it
    You keep full ownership and control of your skills. The hub just makes them discoverable and installable. 💚

🛠 How Your Agent Actually Uses It

After installing, skills land in ~/.ai/skills/, ~/.claude/skills/, or the equivalent directory for your agent. Your AI assistant automatically discovers and loads them in each conversation.

The change in agent behavior is immediate. Ask things like:

  • "Write a ColdBox REST handler with full error handling"
  • "Create a WireBox-managed singleton service that queries SQLite"
  • "Show me how to use TestBox to write integration tests"
  • "Help me configure bx-migrations for my BoxLang app"

…and the agent answers using patterns and idioms from the installed skills, not scattered (and often outdated) snippets pulled from random internet training data. The hallucinations go down. The accuracy goes up. The output starts to feel like it was written by someone who actually knows the framework — because, in a sense, it now was. 🎓

🔮 Why This Matters Beyond BoxLang

We didn't build skills.boxlang.io as a marketing site. We built it because the Ortus ecosystem — BoxLang, ColdBox, TestBox, CommandBox, WireBox, LogBox, CacheBox, hundreds of modules across 18+ years of work — is too rich to fit into anyone's training data, and too valuable to be re-discovered through trial and error every time a developer opens a new chat with their AI assistant.

A public, curated, audited skills directory means:

  • Module authors can ship AI knowledge alongside their code
  • Teams can standardize agent behavior across every developer's workstation
  • Newcomers get accurate, idiomatic guidance from day one
  • The community owns and contributes to a shared knowledge layer that compounds over time

This is the same shift package managers brought to language ecosystems — except for AI knowledge. It's the era of skills, and now every BoxLang and ColdBox developer can participate. 🚀

🎯 Get Started Now

# Install your first skill in 10 seconds
npx skills add ortus-boxlang/skills

# Or via the ColdBox CLI
coldbox ai skills install --list

Then point your AI agent at your codebase and watch the difference. ⚡

📚 Resources

Got a skill you'd love to publish, or one you wish existed? We'd love to hear from you — open a PR, submit your repo, or drop us a note. The directory grows because the community grows. 💚

The post Introducing skills.boxlang.io — The Open Agent Skills Ecosystem for BoxLang & the Ortus World appeared first on foojay.

]]>
https://foojay.io/today/introducing-skills-boxlang-io-the-open-agent-skills-ecosystem-for-boxlang-the-ortus-world/feed/ 0
A New Chapter for the Payara Community https://foojay.io/today/a-new-chapter-for-the-payara-community/ https://foojay.io/today/a-new-chapter-for-the-payara-community/#respond Mon, 18 May 2026 06:51:51 +0000 https://foojay.io/?p=123821 Table of Contents What the acquisition means for the CommunityWhat's changing (and when)Getting out and meeting youWhat's been shipping: April and May 2026 Azul Payara Community ReleasesMay: Azul Payara Community 7.2026.5April: Azul Payara Community 7.2026.4A lot more to come Something ...

The post A New Chapter for the Payara Community appeared first on foojay.

]]>
Table of Contents
What the acquisition means for the CommunityWhat's changing (and when)Getting out and meeting youWhat's been shipping: April and May 2026 Azul Payara Community ReleasesA lot more to come

Something has been in the works since Azul completed its acquisition of Payara in December 2025, and today we're ready to share it: the community edition of Payara has a new name and logo – but not so very different from the one you already know!

Payara Platform Community is now Azul Payara Community, made up of two distributions you already know and love - Azul Payara Server Community and Azul Payara Micro Community - plus the tooling and connectors that go with them.

It's a small change in letters but an important one. The new name reflects where we are: fully part of the Azul family, with all the backing that brings, while staying true to what this project has always been - an open-source runtime built by and for the Java and Jakarta EE community.

The iconic Payara fish has also had a bit of a refresh. The Azul Payara commercial logos were updated back in December, and now the community edition gets the same treatment - same fish character the Payara community knows well, just updated to match its new home at Azul.

What the acquisition means for the Community

We believe the open-source community is the heart of the Payara ecosystem. The contributors, committers and developers using Azul Payara Community for testing, education, side projects or apps that haven't gone commercial yet all matter to us. Growing that community, listening to it and investing in it is central to how we think about Azul Payara's future.

The rebrand is part of bringing Azul Payara Community properly into the Azul portfolio alongside Azul Zulu (OpenJDK), Azul Prime, Intelligence Cloud and Azul Payara’s commercial offering. It's the same open-source project with a new home in the broader Azul ecosystem.

What's changing (and when)

Over the coming weeks and months, you can expect to see updates to Payara documentation, resource names, technical content and the blog. Downloads are still available at payara.fish for now, but will be moving to Azul website before long - we'll announce that when the time comes.

One thing we're particularly excited about: we'll be increasing our presence here on Foojay sharing everything that is relevant for the Friends of OpenJDK community - educational content, tutorials, community updates and more.

For social media, we're consolidating onto Foojay and Azul's official channels. Make sure you're following us there, so you don't miss a thing.

Getting out and meeting you

Together with the Azul DevRel, Product and Engineering Teams, we're planning to visit a lot of Java User Groups over the coming months, and we're really looking forward to meeting community members face to face. If your JUG would like a visit or a talk on Azul Payara Community, OpenJDK or Jakarta EE - let us know.

We'll also be at a number of Java conferences this year. More details to come, but if you spot us - come and say hello.

What's been shipping: April and May 2026 Azul Payara Community Releases

We didn't want to announce the rebrand without also catching you up on recent releases (download here!), so here's a combined look at what landed for Azul Payara Community in April and May.

May: Azul Payara Community 7.2026.5

Security fixes (critical - please upgrade):

  • Remote arbitrary file read vulnerability via unsafe parsing of OpenMQ configuration
  • Restricted access to vulnerable EL expressions

Bug fixes:

  • Admin Console freezing after upgrading from Payara 6 to 7

Improvements:

  • Updated JACC Provider Compatibility Startup Service

  • Audit Modules removed

  • warlibs support added to Admin Console redeployment

  • Reduced INFO logging for the Jakarta Data implementation

  • New deployment descriptors created with deprecated properties removed

  • Fix for Jakarta Data @Repository methods not throwing UnsupportedOperationException when no implementation logic can be injected at deploy time

Component upgrades: Docker JDK images refreshed to 21.0.11 and 25.0.3, with dependency updates for Jakarta Faces, MicroProfile Config, Project Reactor, and other libraries.

The critical security fix is also backported across Azul Payara 6.38.0, 5.87.0, and 4.1.2.191.55 — we recommend all users upgrade regardless of which branch they're on.

April: Azul Payara Community 7.2026.4

April's community release was a significant cleanup milestone, removing three long-standing deprecated items: the start-domain --upgrade service (replaced by the Payara Upgrade Tool), all methods previously annotated @Deprecated, and all deprecated configuration properties.

Bug fixes:

  • Asadmin Recorder generating invalid commands when recording MicroProfile property changes

  • Rendering issue in the Admin Console connection pool Advanced tab

  • Broken news link in the Admin Console

  • Race condition in application-scoped QueryData under concurrent access

  • OpenMQ unclosed stream warnings

Community contributions from Lenny Primak:

  • Fix for CDI annotation type resolution failing when annotations were defined in WAR library dependencies
  • Resolution of an SLF4J class loader leak that could accumulate memory in long-running deployments

Component upgrades: EclipseLink 5.0.0-B08 → 5.0.0-B13, OpenMQ updated to 6.8.0, plus bumps to Jackson BOM, Reactor Core, Kotlin Stdlib, and several others.

A lot more to come

The rebrand is just the start. As part of Azul, Payara Community gains access to more resources, more engineering investment and a broader platform to grow.

We have exciting plans - for the runtime, the tooling and connectors available to community users, content, and the events we put on - and we'll be sharing them with you as they take shape.

A huge thank you to everyone who has been part of the Payara community over the years - the contributors, the committers, the developers who have filed issues, submitted fixes, written content and shown up at conferences and JUGs. This project is what it is because of you, and that doesn't change with a new name. We're genuinely excited about what comes next, and we hope you are too!

For now: download the latest release from payara.fish, join us on Foojay, and follow Azul's official social channels for updates.

The post A New Chapter for the Payara Community appeared first on foojay.

]]>
https://foojay.io/today/a-new-chapter-for-the-payara-community/feed/ 0
AI-Powered Code Review Assistant: Automated Code Analysis with Spring AI and MongoDB https://foojay.io/today/ai-powered-code-review-assistant-automated-code-analysis-with-spring-ai-and-mongodb/ https://foojay.io/today/ai-powered-code-review-assistant-automated-code-analysis-with-spring-ai-and-mongodb/#respond Thu, 14 May 2026 17:09:39 +0000 https://foojay.io/?p=123693 Table of Contents Prerequisites1. Project setup2. Storing and managing review patternsDefining the pattern modelCreating the repositoryBuilding the service layerExposing the REST endpoints3. Embedding patterns with Spring AI and MongoDB Atlas Vector SearchAdding Spring AI dependenciesGenerating embeddingsSeeding the pattern libraryCreating the ...

The post AI-Powered Code Review Assistant: Automated Code Analysis with Spring AI and MongoDB appeared first on foojay.

]]>
Table of Contents
Prerequisites1. Project setup2. Storing and managing review patterns3. Embedding patterns with Spring AI and MongoDB Atlas Vector Search4. Building the code review engine5. Tracking review trends with aggregation pipelines6. Testing the full workflowConclusion

Code reviews catch bugs before they ship, but they take time. Most teams rely on manual review or basic linters that flag syntax issues but miss deeper problems like subtle resource leaks, poor exception handling, or security anti-patterns. Static analysis tools help, but they work with rigid rules that cannot generalize across code variations. A rule that catches catch (Exception e) {} will miss catch (Throwable t) { return null; }, even though both are the same underlying problem.

In this article, you will build a code review assistant API. Developers submit code snippets through a REST endpoint. The system embeds the submitted code with Spring AI and searches a library of known anti-patterns stored as vectors in MongoDB Atlas. It then sends the code along with matched patterns to an LLM for structured review feedback. Every submission and its findings are stored in MongoDB, and aggregation pipelines surface trends over time.

The tech stack is Java 21+, Spring Boot 3.x, Spring AI, Spring Data MongoDB, and MongoDB Atlas. By the end, you will have a working review API that accepts code, finds relevant anti-patterns using Atlas Vector Search, gets structured feedback from an LLM, and tracks findings across submissions. The complete source code is available in the companion repository on GitHub.

Prerequisites

  • Java 21 or later
  • Spring Boot 3.x (use Spring Initializr with the Spring Data MongoDB and Spring Web dependencies; you will add Spring AI manually later in the article)
  • A MongoDB Atlas cluster (the free tier is sufficient, and you will need it for Atlas Vector Search). You can set up one by following the MongoDB Atlas getting started guide.
  • An OpenAI API key (used for both the embedding model and the chat model)
  • Basic familiarity with Spring Boot (controllers, services, dependency injection)

1. Project setup

Go to Spring Initializr and generate a new project. I am using the following settings, feel free to use your own group name:

  • Group: dev.farhan
  • Artifact: code-review-assistant
  • Java version: 21
  • Dependencies: Spring WebSpring Data MongoDB

You will add Spring AI dependencies manually in section 3. For now, the project only needs web and MongoDB support.

Open application.properties and configure the MongoDB connection:

spring.data.mongodb.uri=mongodb+srv://<username>:<password>@<cluster>.mongodb.net/code-review-assistant?appName=devrel-article-java-springai-foojay

Replace the placeholders with your Atlas cluster credentials. The appName query parameter helps MongoDB track which application is connecting, which is useful for monitoring. If you are running MongoDB locally, use mongodb://localhost:27017/code-review-assistant?appName=devrel-article-java-springai-foojay instead.

The companion repository has the complete project structure. You can clone it and follow along, or build each piece from scratch as you read.

2. Storing and managing review patterns

The review assistant works by comparing submitted code against a library of known anti-patterns. Before you can do any comparison, you need a way to define what an anti-pattern looks like, store it in MongoDB, and expose endpoints for adding and listing patterns.

Defining the pattern model

Review findings will have severity levels, so start by defining those as a Java enum. An enum is a type that restricts a value to a fixed set of options, which prevents invalid severity strings from entering the system:

public enum Severity {
    CRITICAL, WARNING, INFO
}

CRITICAL is for issues that will cause bugs or security vulnerabilities. WARNING is for problems that may cause issues under certain conditions. INFO is for suggestions that improve code quality but are not urgent.

Next, define the ReviewPattern class. This is the document that represents a single anti-pattern in your library. The @Document annotation tells Spring Data MongoDB which collection this class maps to, and @Id marks the field that MongoDB will use as the document's unique identifier:

@Document(collection = "review_patterns")
public class ReviewPattern {

    @Id
    private String id;
    private String name;
    private String description;
    private String language;
    private Severity severity;
    private String category;
    private String exampleBadCode;
    private String exampleGoodCode;
    private String explanation;

    // constructors, getters, and setters omitted for brevity
}

Each pattern has a name (like "empty catch block"), a description that explains the problem in plain language, and a language field so you can filter patterns by programming language. The category field groups related issues together (for example, "security" or "error-handling"). The exampleBadCode and exampleGoodCode fields show the problem and its fix side by side, and explanation describes why the bad code is problematic.

You will add an embedding field to this class later in section 3 when you set up vector search. For now, the text fields are enough to define the pattern library.

Each pattern's id is a human-readable slug like unclosed-resources or hardcoded-credentials, set at creation time rather than auto-generated as an ObjectId. ObjectIds are useful when many writers insert records concurrently or when you want a time-ordered index, but neither is an issue with a small admin-curated pattern library. Slugs make findings easier to read in the shell and give the LLM a meaningful label to echo back in matchedPatternId.

To see what a pattern looks like as a JSON document, here are two examples. The first describes an empty catch block, a common error-handling problem:

{
  "_id": "empty-catch-block",
  "name": "Empty catch block",
  "description": "Catching an exception and doing nothing with it, silently swallowing errors",
  "language": "java",
  "severity": "CRITICAL",
  "category": "error-handling",
  "exampleBadCode": "try { connection.close(); } catch (SQLException e) { }",
  "exampleGoodCode": "try { connection.close(); } catch (SQLException e) { logger.warn(\"Failed to close: {}\", e.getMessage()); }",
  "explanation": "Empty catch blocks silently swallow errors. When something fails, there is no log entry and no way to diagnose the problem."
}
``````json
{
  "_id": "hardcoded-credentials",
  "name": "Hardcoded credentials",
  "description": "Storing passwords, API keys, or secrets as string literals in source code",
  "language": "java",
  "severity": "CRITICAL",
  "category": "security",
  "exampleBadCode": "private static final String DB_PASSWORD = \"s3cretP@ss!\";",
  "exampleGoodCode": "@Value(\"${db.password}\") private String dbPassword;",
  "explanation": "Hardcoded credentials end up in version control and build artifacts. Use environment variables or a secrets manager."
}

The second describes hardcoded credentials, a security anti-pattern:

{
  "_id": "hardcoded-credentials",
  "name": "Hardcoded credentials",
  "description": "Storing passwords, API keys, or secrets as string literals in source code",
  "language": "java",
  "severity": "CRITICAL",
  "category": "security",
  "exampleBadCode": "private static final String DB_PASSWORD = \"s3cretP@ss!\";",
  "exampleGoodCode": "@Value(\"${db.password}\") private String dbPassword;",
  "explanation": "Hardcoded credentials end up in version control and build artifacts. Use environment variables or a secrets manager."
}

Each JSON document maps directly to the fields in the ReviewPattern class. When you save one of these through the API, Spring Data MongoDB converts the Java object into a document with this same structure and stores it in the review_patterns collection.

Creating the repository

To read and write patterns from MongoDB, you need a repository interface. In Spring Data, a repository is an interface that provides database operations without requiring you to write implementation code. You declare methods with names that follow a specific naming convention, and Spring generates the query logic at runtime:

public interface ReviewPatternRepository extends MongoRepository<ReviewPattern, String> {

    List<ReviewPattern> findByLanguage(String language);

    List<ReviewPattern> findByCategory(String category);

    List<ReviewPattern> findByLanguageAndCategory(String language, String category);

    List<ReviewPattern> findBySeverity(Severity severity);
}

By extending MongoRepository<ReviewPattern, String>, this interface inherits standard operations like save()findById()findAll(), and deleteById(). The two generic parameters tell Spring that this repository manages ReviewPattern documents and that the ID field is a String.

The custom methods use Spring Data's derived query feature. findByLanguage("java") translates to a MongoDB query that filters documents where the language field equals "java"findByLanguageAndCategory combines two filters with an AND condition. You do not need to write any MongoDB query syntax here. Spring parses the method name, identifies the field names and the operator (And), and builds the query for you.

Building the service layer

The service class contains the business logic for creating and retrieving patterns. The @Service annotation marks it as a Spring-managed component, which means Spring will create a single instance of this class and make it available for injection into other components:

@Service
public class ReviewPatternService {

    private final ReviewPatternRepository patternRepository;

    public ReviewPatternService(ReviewPatternRepository patternRepository) {
        this.patternRepository = patternRepository;
    }

    public ReviewPattern createPattern(CreatePatternRequest request) {
        ReviewPattern pattern = new ReviewPattern(
                request.id(), request.name(), request.description(), request.language(),
                request.severity(), request.category(),
                request.exampleBadCode(), request.exampleGoodCode(),
                request.explanation()
        );
        return patternRepository.save(pattern);
    }

    public List<ReviewPattern> listPatterns(String language, String category) {
        if (language != null && category != null) {
            return patternRepository.findByLanguageAndCategory(language, category);
        }
        if (language != null) {
            return patternRepository.findByLanguage(language);
        }
        if (category != null) {
            return patternRepository.findByCategory(category);
        }
        return patternRepository.findAll();
    }

    public Optional<ReviewPattern> getPattern(String id) {
        return patternRepository.findById(id);
    }
}

The constructor takes a ReviewPatternRepository as a parameter. Spring sees this and automatically injects the repository instance it created. This pattern is called constructor injection, and it is the recommended way to wire dependencies in Spring Boot.

The createPattern method builds a ReviewPattern from the incoming request and saves it to MongoDB.

The listPatterns method handles optional filtering. When both language and category are provided as query parameters, it calls the combined query. Without that first check, the method would silently ignore the category and filter by language only. When neither filter is provided, it falls back to findAll(), which returns every pattern in the collection.

The getPattern method returns an Optional<ReviewPattern>. An Optional is a container that may or may not hold a value. It forces the caller to handle the case where no pattern exists for the given ID, rather than risking a null pointer exception.

The CreatePatternRequest is a Java record that maps the incoming JSON request body. Records are a concise way to define immutable data carriers. The compiler automatically generates a constructor, getter methods, and equals/hashCode implementations from the field list:

public record CreatePatternRequest(
        String id, String name, String description, String language,
        Severity severity, String category,
        String exampleBadCode, String exampleGoodCode, String explanation
) {}

When a JSON body arrives at the endpoint, Spring deserializes it into this record by matching JSON field names to the record's component names.

Exposing the REST endpoints

The controller class maps HTTP requests to service methods. The @RestController annotation tells Spring that this class handles web requests and that every method's return value should be serialized directly as the response body (as JSON, by default). @RequestMapping("/api/patterns") sets the base URL path for all endpoints in this controller:

@RestController
@RequestMapping("/api/patterns")
public class ReviewPatternController {

    private final ReviewPatternService patternService;

    public ReviewPatternController(ReviewPatternService patternService) {
        this.patternService = patternService;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ReviewPattern createPattern(@RequestBody CreatePatternRequest request) {
        return patternService.createPattern(request);
    }

    @GetMapping
    public List<ReviewPattern> listPatterns(
            @RequestParam(required = false) String language,
            @RequestParam(required = false) String category) {
        return patternService.listPatterns(language, category);
    }

    @GetMapping("/{id}")
    public ReviewPattern getPattern(@PathVariable String id) {
        return patternService.getPattern(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }
}

@PostMapping handles POST requests to /api/patterns. The @RequestBody annotation tells Spring to deserialize the JSON request body into a CreatePatternRequest record. @ResponseStatus(HttpStatus.CREATED) changes the default response code from 200 to 201, which is the standard HTTP status for "resource created."

@GetMapping without a path handles GET requests to /api/patterns. The @RequestParam(required = false) annotation binds query parameters from the URL. For example, GET /api/patterns?language=java&category=security passes "java" as language and "security" as category. Since both are marked as not required, omitting them results in null values, which the service handles by returning all patterns.

@GetMapping("/{id}") handles GET requests like /api/patterns/unclosed-resources. The @PathVariable annotation extracts the id value from the URL path. If the service returns an empty OptionalorElseThrow converts it into a 404 response.

You can test this by adding a pattern manually:

curl -X POST http://localhost:8080/api/patterns \
  -H "Content-Type: application/json" \
  -d '{
    "id": "empty-catch-block",
    "name": "Empty catch block",
    "description": "Catching an exception and doing nothing with it",
    "language": "java",
    "severity": "CRITICAL",
    "category": "error-handling",
    "exampleBadCode": "try { conn.close(); } catch (SQLException e) { }",
    "exampleGoodCode": "try { conn.close(); } catch (SQLException e) { logger.warn(\"Close failed\", e); }",
    "explanation": "Empty catch blocks silently swallow errors."
  }'

This works for adding patterns one at a time, but the system is more useful with a full library loaded. The next section adds the data seeder along with the embedding and vector search capabilities that make pattern matching work.

3. Embedding patterns with Spring AI and MongoDB Atlas Vector Search

Suppose a developer writes InputStream is = new FileInputStream(path); without a try-with-resources block. Your pattern library describes "unclosed resources in try blocks" with a different code example that uses FileReader. The underlying problem is identical, but the code looks different. Exact string matching will not connect the two. This is where embeddings help. By converting both the stored pattern and the submitted code into vectors, you can measure their semantic similarity regardless of superficial differences in syntax.

Adding Spring AI dependencies

Spring AI is managed through a Bill of Materials (BOM), which is a special dependency declaration that locks the versions of all Spring AI modules so they stay compatible with each other. Add the BOM and the OpenAI starter to your pom.xml:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- existing dependencies -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
</dependencies>

The spring-ai-starter-model-openai dependency does not have a <version> tag. The BOM provides the version, so you only need to specify it in one place. The starter auto-configures both an EmbeddingModel bean (for generating vectors) and a ChatClient.Builder bean (for calling the LLM), which you will use in later sections.

Then add the OpenAI configuration to application.properties:

spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.embedding.options.model=text-embedding-3-small
spring.ai.openai.chat.options.model=gpt-4o-mini
spring.ai.openai.chat.options.temperature=0.2

The ${OPENAI_API_KEY} syntax reads the value from an environment variable, so you do not hardcode your key in the configuration file. The text-embedding-3-small model produces 1536-dimensional vectors, meaning each piece of text gets converted into an array of 1536 numbers that capture its semantic meaning. The low temperature setting (0.2) keeps code review output deterministic and consistent, which is what you want for a review tool that should give similar feedback for similar code. You can swap gpt-4o-mini for a different model if you want stronger results and do not mind higher API costs.

Generating embeddings

To generate an embedding for a pattern, you need to combine its most descriptive fields into a single text block and pass that to the embedding model. Add an embedding field and a helper method to the ReviewPattern class:

@Document(collection = "review_patterns")
public class ReviewPattern {

    // ... existing fields ...

    private float[] embedding;

    public float[] getEmbedding() { return embedding; }
    public void setEmbedding(float[] embedding) { this.embedding = embedding; }

    public String buildEmbeddingText() {
        return description + " " + exampleBadCode + " " + explanation;
    }
}

The embedding field stores the vector that the embedding model generates. It is a float[] because each dimension is a floating-point number.

buildEmbeddingText() concatenates the description, example bad code, and explanation into one string. This gives the embedding model enough context to understand what the pattern is about. The method lives on the model class because both the service and the data seeder need to build this text, and putting it here means the concatenation logic is defined in one place. If you later decide to include the pattern name or category in the embedding, you change this one method instead of updating it in multiple places.

Now update the ReviewPatternService to inject the EmbeddingModel and generate embeddings when creating patterns:

@Service
public class ReviewPatternService {

    private final ReviewPatternRepository patternRepository;
    private final EmbeddingModel embeddingModel;

    public ReviewPatternService(ReviewPatternRepository patternRepository,
                                EmbeddingModel embeddingModel) {
        this.patternRepository = patternRepository;
        this.embeddingModel = embeddingModel;
    }

    public ReviewPattern createPattern(CreatePatternRequest request) {
        ReviewPattern pattern = new ReviewPattern(
                request.id(), request.name(), request.description(), request.language(),
                request.severity(), request.category(),
                request.exampleBadCode(), request.exampleGoodCode(),
                request.explanation()
        );

        pattern.setEmbedding(embeddingModel.embed(pattern.buildEmbeddingText()));

        return patternRepository.save(pattern);
    }

    // listPatterns and getPattern remain unchanged
}

The EmbeddingModel is a Spring AI interface that the OpenAI starter auto-configures. Its embed() method sends the text to OpenAI's embedding API and returns a float[] with 1536 values. Each value represents one dimension of the text's meaning in the model's vector space. Two pieces of text about similar topics will produce vectors that point in similar directions, which is what makes semantic search possible.

Seeding the pattern library

The companion repository includes a DataSeeder component that loads about 20 patterns on startup. It implements CommandLineRunner, which is a Spring Boot interface with a single run method. Spring Boot automatically calls run after the application context is fully initialized, making it a convenient place for one-time setup tasks like loading seed data:

@Component
public class DataSeeder implements CommandLineRunner {

    private final ReviewPatternRepository patternRepository;
    private final EmbeddingModel embeddingModel;

    public DataSeeder(ReviewPatternRepository patternRepository,
                      EmbeddingModel embeddingModel) {
        this.patternRepository = patternRepository;
        this.embeddingModel = embeddingModel;
    }

    @Override
    public void run(String... args) {
        if (patternRepository.count() > 0) {
            return;
        }

        List<ReviewPattern> patterns = createPatterns();

        for (ReviewPattern pattern : patterns) {
            pattern.setEmbedding(embeddingModel.embed(pattern.buildEmbeddingText()));
        }

        patternRepository.saveAll(patterns);
    }

    private List<ReviewPattern> createPatterns() {
        List<ReviewPattern> patterns = new ArrayList<>();

        patterns.add(new ReviewPattern(
                "unclosed-resources",
                "Unclosed resources",
                "Opening a resource without using try-with-resources",
                "java", Severity.CRITICAL, "maintainability",
                "FileInputStream fis = new FileInputStream(\"config.properties\");\n"
                + "Properties props = new Properties();\n"
                + "props.load(fis);\nreturn props;",
                "try (FileInputStream fis = new FileInputStream(\"config.properties\")) {\n"
                + "    Properties props = new Properties();\n"
                + "    props.load(fis);\n    return props;\n}",
                "If an exception occurs between opening and closing a resource, "
                + "the close call never runs. This leaks file handles and connections."
        ));

        // ... 19 more patterns covering error-handling, security,
        //     performance, and maintainability categories ...

        return patterns;
    }
}

The run method starts with a guard check: patternRepository.count() > 0. If the collection already has data, the method returns immediately. This prevents the seeder from re-generating embeddings or re-inserting data on application restarts.

When the collection is empty, the method builds all 20 patterns, then loops through each one to generate its embedding. The loop calls embeddingModel.embed() once per pattern, sending each pattern's text to the OpenAI API. After all embeddings are generated, patternRepository.saveAll(patterns) writes every pattern to MongoDB in a single batch operation, which is more efficient than saving them one at a time in separate round trips.

The full list of 20 patterns covers error handling (catching generic exceptions, empty catch blocks, swallowing InterruptedException), security (hardcoded credentials, SQL injection, logging sensitive data), performance (string concatenation in loops, N+1 queries, unnecessary autoboxing), and maintainability (unclosed resources, missing null checks, raw generics). The complete list is available in the companion repository.

Creating the Atlas Vector Search index

Before you can query the embeddings, you need to create a vector search index in Atlas. This index tells MongoDB how to organize and search the embedding vectors efficiently.

Go to your cluster in the Atlas UI, select the Atlas Search tab, and click Create Search Index. Choose Atlas Vector Search as the index type and select the review_patterns collection. In the index name field, enter vector_index. The code you write later references the index by this exact name, so do not leave the auto-generated default. Then paste the following definition:

{
  "fields": [
    {
      "type": "vector",
      "path": "embedding",
      "numDimensions": 1536,
      "similarity": "cosine"
    }
  ]
}

The path field points to embedding, which is where you stored the vector in the ReviewPattern class. The numDimensions value must match the output of your embedding model, which is 1536 for text-embedding-3-small. If these values do not match, the search will fail.

The similarity field specifies how MongoDB measures the distance between vectors. Cosine similarity measures the angle between two vectors regardless of their magnitude, which makes it a good fit for text embeddings where the direction of the vector matters more than its length.

Searching for similar patterns

With the index in place, you can build a method that finds patterns semantically similar to a given code snippet. This method takes a query vector (the embedding of the submitted code) and runs a $vectorSearch aggregation against the patterns collection.

Aggregation pipelines in MongoDB work like an assembly line. Data flows through a sequence of stages, and each stage transforms the data before passing it to the next one. In this pipeline, the first stage finds similar vectors, the second adds a similarity score to each result, and the third removes the large embedding array from the output:

private List<ReviewPattern> findSimilarPatterns(float[] queryVector, int limit) {
    List<Double> queryVectorList = new ArrayList<>();
    for (float f : queryVector) {
        queryVectorList.add((double) f);
    }

    Document vectorSearchStage = new Document("$vectorSearch",
            new Document("index", "vector_index")
                    .append("path", "embedding")
                    .append("queryVector", queryVectorList)
                    .append("numCandidates", 50)
                    .append("limit", limit));

    AggregationOperation vectorSearch = context -> vectorSearchStage;

    AggregationOperation addScore = context ->
            new Document("$addFields",
                    new Document("searchScore",
                            new Document("$meta", "vectorSearchScore")));

    AggregationOperation excludeEmbedding = context ->
            new Document("$project",
                    new Document("embedding", 0));

    Aggregation aggregation = Aggregation.newAggregation(vectorSearch, addScore, excludeEmbedding);

    AggregationResults<ReviewPattern> results =
            mongoTemplate.aggregate(aggregation, "review_patterns", ReviewPattern.class);

    return results.getMappedResults();
}

The method starts by converting the float[] query vector into a List<Double>. This conversion is necessary because the MongoDB Java driver expects double-precision numbers in the $vectorSearch query vector.

The $vectorSearch stage is the core of this method. It specifies which index to use (vector_index), which field contains the vectors (embedding), and the query vector to compare against. The numCandidates parameter controls how many candidate documents MongoDB evaluates internally before selecting the final results. Setting it higher than limit gives the search algorithm more options to choose from, which improves accuracy at the cost of slightly more processing time. The limit parameter controls how many results to return.

The $addFields stage adds a searchScore field to each result. The $meta: "vectorSearchScore" expression pulls the cosine similarity score that MongoDB calculated during the vector search. This score ranges from 0 to 1, where 1 means the vectors are identical. You will pass this score to the LLM later so it knows how confident the vector search was about each match.

The $project stage with "embedding": 0 removes the embedding array from the results. Each embedding is a 1536-element array that the prompt builder does not need, so without this exclusion, every vector search would transfer several kilobytes of unused data per pattern.

Finally, mongoTemplate.aggregate() runs the pipeline against the review_patterns collection and maps each result document back into a ReviewPattern Java object.

To hold the similarity score that $addFields injects, add a searchScore field to ReviewPattern and mark it with @Transient:

@Transient
private double searchScore;

The @Transient annotation tells Spring Data MongoDB not to persist this field to the database. The searchScore only gets populated during vector search results and has no meaning outside that context. Without @Transient, saving a pattern returned by vector search would write a stale score to the database.

4. Building the code review engine

The ReviewService is where the pieces connect. It accepts a code submission, finds matching patterns via vector search, sends both to an LLM, and parses the structured response into findings. The following diagram shows the complete flow from submission to response:

Figure 1: Review flow diagram showing the steps from code submission through embedding, vector search, LLM analysis, and saving findings to MongoDB

Before building the service, you need two more document classes: one for storing the code that developers submit, and one for storing the issues that the review engine identifies.

Defining the submission and finding models

The CodeSubmission document stores each code snippet that a developer sends for review:

@Document(collection = "code_submissions")
public class CodeSubmission {

    @Id
    private String id;
    private String code;
    private String language;
    private String fileName;
    private String submittedBy;
    private Instant submittedAt;
    private List<String> findingIds;

    // constructors, getters, and setters omitted for brevity
}

The code field holds the raw source code the developer submits. The language and fileName fields provide context about what kind of code it is. The submittedAt field uses Instant, which stores a precise UTC timestamp. The findingIds field is a list of references to the ReviewFinding documents that the review produces. Rather than embedding findings inside the submission document, storing IDs keeps the submission document small and lets you query findings independently.

The ReviewFinding document stores individual issues that the review engine identifies. Each finding references its parent submission and optionally references the pattern it matched:

@Document(collection = "review_findings")
public class ReviewFinding {

    @Id
    private String id;
    @Indexed
    private String submissionId;
    private String matchedPatternId;
    private int startLine;
    private int endLine;
    private Severity severity;
    private String category;
    private String message;
    private String suggestion;
    private double confidence;

    // constructors, getters, and setters omitted for brevity
}

The @Indexed annotation on submissionId tells Spring Data MongoDB to create a database index on that field. When you look up all findings for a given submission, MongoDB uses this index to jump directly to the matching documents instead of scanning the entire collection. Without it, every call to findBySubmissionId would get slower as the collection grows.

The startLine and endLine fields mark where in the submitted code the issue appears. The matchedPatternId field is nullable because the LLM may flag issues that do not map to any stored pattern. For example, the LLM might notice a logic error that is too specific to be a general anti-pattern. The confidence field is a score from 0.0 to 1.0 that the LLM assigns to indicate how certain it is about the finding.

The review service

Here is the flow that the review service follows for each submission:

  1. Save the code submission to MongoDB.
  2. Embed the submitted code and run vector search to find the top 5 matching patterns.
  3. Build a prompt with the code and matched patterns, then call the LLM.
  4. Parse the LLM response into ReviewFinding objects and save them.
@Service
public class ReviewService {

    private final MongoTemplate mongoTemplate;
    private final EmbeddingModel embeddingModel;
    private final ChatClient chatClient;
    private final CodeSubmissionRepository submissionRepository;
    private final ReviewFindingRepository findingRepository;

    public ReviewService(MongoTemplate mongoTemplate,
                         EmbeddingModel embeddingModel,
                         ChatClient.Builder chatClientBuilder,
                         CodeSubmissionRepository submissionRepository,
                         ReviewFindingRepository findingRepository) {
        this.mongoTemplate = mongoTemplate;
        this.embeddingModel = embeddingModel;
        this.chatClient = chatClientBuilder.build();
        this.submissionRepository = submissionRepository;
        this.findingRepository = findingRepository;
    }

    public ReviewResponse reviewCode(ReviewRequest request) {
        if (request.code() == null || request.code().isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Code must not be empty");
        }

        CodeSubmission submission = new CodeSubmission();
        submission.setCode(request.code());
        submission.setLanguage(request.language());
        submission.setFileName(request.fileName());
        submission.setSubmittedAt(Instant.now());

        float[] codeEmbedding = embeddingModel.embed(request.code());
        List<ReviewPattern> matchedPatterns = findSimilarPatterns(codeEmbedding, 5);

        String systemPrompt = buildSystemPrompt();
        String userPrompt = buildUserPrompt(request.code(), matchedPatterns);

        List<ReviewFinding> findings = chatClient.prompt()
                .system(systemPrompt)
                .user(userPrompt)
                .call()
                .entity(new ParameterizedTypeReference<>() {});

        submission = submissionRepository.save(submission);

        for (ReviewFinding finding : findings) {
            finding.setSubmissionId(submission.getId());
        }
        List<ReviewFinding> savedFindings = findingRepository.saveAll(findings);
        List<String> findingIds = savedFindings.stream()
                .map(ReviewFinding::getId)
                .toList();

        submission.setFindingIds(findingIds);
        submissionRepository.save(submission);

        return new ReviewResponse(submission, savedFindings);
    }

    // findSimilarPatterns from section 3 goes here
    // buildSystemPrompt and buildUserPrompt shown below
}

The constructor takes five dependencies. The ChatClient.Builder is a Spring AI auto-configured bean that provides a builder for creating chat clients. The service calls .build() in the constructor to create a ChatClient instance that it reuses for every review request. MongoTemplate provides lower-level MongoDB operations that the repository interfaces do not cover, which you need for the vector search aggregation pipeline.

The reviewCode method starts with a null check on the submitted code. Without it, an empty request would trigger an expensive embedding API call and LLM call before eventually failing. Returning a 400 error early is cheaper and gives the caller a clear error message.

Next, the method creates a CodeSubmission object and populates its fields from the request. It then generates an embedding for the submitted code using the same embeddingModel.embed() method used for patterns. This vector represents the semantic meaning of the code, and findSimilarPatterns uses it to search for patterns whose embeddings point in a similar direction.

The chatClient.prompt() chain builds and sends the LLM request. .system(systemPrompt) sets the system-level instructions that define how the LLM should behave. .user(userPrompt) provides the actual code and matched patterns. .call() sends the request to the OpenAI API. .entity(new ParameterizedTypeReference<>() {}) tells Spring AI to parse the LLM's JSON response directly into a List<ReviewFinding>. Spring AI generates the JSON schema from the target type and instructs the LLM to return JSON in that format, so you do not need to write parsing code yourself.

After the LLM returns findings, the method saves the submission first to get its generated id, then assigns that id to each finding before saving them with findingRepository.saveAll(). Using saveAll in a single batch is more efficient than saving each finding individually, since batch saving makes one database round trip instead of one per finding. Finally, the submission is updated with the list of finding IDs and saved again.

The method returns savedFindings (the list from saveAll) rather than the original findings list. The saved list has MongoDB-generated IDs on each finding. Returning the original list would give clients findings without IDs, making it harder to reference specific findings later.

One catch with the two saves is that if the application crashes between them, the findings will be in the database with a valid submissionId, but the submission document will have an empty findingIds. The data is not lost, though. Each finding still references its parent, so findingRepository.findBySubmissionId(submission.getId()) returns them and you can rebuild the submission's findingIds afterward. If you want stricter atomicity, wrap both writes in a MongoDB multi-document transaction with Spring's @Transactional. Otherwise, treat findingIds as a lookup optimization and query by submissionId as a fallback.

Prompt design

The system prompt sets the reviewer persona and defines the exact output format. Being specific about the JSON structure is important because the entity() call on the chat client needs the response to match the ReviewFinding class:

private String buildSystemPrompt() {
    return """
        You are a senior Java code reviewer. Analyze the submitted code and identify issues.
        You will receive a code snippet and a set of known anti-patterns that matched semantically.
        For each issue you find, return a JSON array of findings. Each finding must have these fields:
        - startLine (int): the line number where the issue starts
        - endLine (int): the line number where the issue ends
        - severity (string): one of "CRITICAL", "WARNING", or "INFO"
        - category (string): one of "security", "performance", "maintainability", "error-handling"
        - message (string): a concise description of the issue
        - suggestion (string): how to fix the issue
        - confidence (double): your confidence from 0.0 to 1.0
        - matchedPatternId (string or null): the pattern ID if it matches a provided pattern

        Focus on real issues. Do not flag stylistic preferences or minor formatting.
        Return ONLY the JSON array, no additional text.
        """;
}

The last two lines are important. "Focus on real issues" prevents the LLM from flagging every minor style choice as a finding. "Return ONLY the JSON array" ensures the response is parseable by Spring AI's entity() method. Without that instruction, the LLM might wrap the JSON in markdown code fences or add explanatory text around it, which would break parsing.

The user prompt provides the code to review and the matched patterns from vector search:

private String buildUserPrompt(String code, List<ReviewPattern> patterns) {
    StringBuilder prompt = new StringBuilder();
    prompt.append("## Code to review\n\n```java\n");
    prompt.append(code);
    prompt.append("\n```\n\n");
    prompt.append("## Known anti-patterns to check against\n\n");

    for (int i = 0; i < patterns.size(); i++) {
        ReviewPattern pattern = patterns.get(i);
        prompt.append(String.format("%d. **%s** (ID: %s, similarity: %.3f)\n",
                i + 1, pattern.getName(), pattern.getId(), pattern.getSearchScore()));
        prompt.append("   Description: ").append(pattern.getDescription()).append("\n");
        prompt.append("   Example: ```java\n   ").append(pattern.getExampleBadCode());
        prompt.append("\n   ```\n");
        prompt.append("   Why: ").append(pattern.getExplanation()).append("\n\n");
    }

    return prompt.toString();
}

The prompt includes each pattern's ID so the LLM can populate the matchedPatternId field in its findings. This creates a traceable link from each issue back to the stored pattern that triggered it. The similarity score from vector search is included too, which gives the LLM a signal about how confident the match is. A pattern with a 0.92 similarity score deserves more weight than one at 0.61, and the LLM can factor that into its confidence assessment.

The chatClient.prompt() call can fail if the OpenAI service is unavailable or if the response does not parse into the expected structure. In this tutorial, the exception propagates as a 500 error. In production, you would want to catch the failure and return a meaningful error response to the caller rather than an unhandled stack trace.

The review controller

The controller exposes three endpoints: one for submitting code for review, one for retrieving a past review by submission ID, and one for listing just the findings:

@RestController
@RequestMapping("/api/reviews")
public class ReviewController {

    private final ReviewService reviewService;

    public ReviewController(ReviewService reviewService) {
        this.reviewService = reviewService;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ReviewResponse submitReview(@RequestBody ReviewRequest request) {
        return reviewService.reviewCode(request);
    }

    @GetMapping("/{submissionId}")
    public ReviewResponse getReview(@PathVariable String submissionId) {
        return reviewService.getReview(submissionId);
    }

    @GetMapping("/{submissionId}/findings")
    public List<ReviewFinding> getFindings(@PathVariable String submissionId) {
        return reviewService.getFindings(submissionId);
    }
}

The POST endpoint at /api/reviews accepts a JSON body with the code to review and returns the full review response including the submission and all findings. The GET endpoint at /api/reviews/{submissionId} retrieves a previous review, and /api/reviews/{submissionId}/findings returns just the findings for a given submission, which is useful when you only need the issues without the submission metadata.

Testing the review engine

Submit a Java method with a few intentional issues:

curl -X POST http://localhost:8080/api/reviews \
  -H "Content-Type: application/json" \
  -d '{
    "code": "public void processFile(String path) {\n    String content = \"\";\n    try {\n        FileInputStream fis = new FileInputStream(path);\n        byte[] data = fis.readAllBytes();\n        content = new String(data);\n    } catch (Exception e) {\n        // handle later\n    }\n    String[] lines = content.split(\"\\n\");\n    String result = \"\";\n    for (String line : lines) {\n        result += line.trim() + \"\\n\";\n    }\n    System.out.println(result);\n}",
    "language": "java"
  }'

This code has three issues: an unclosed FileInputStream (no try-with-resources), a generic catch (Exception e) with an empty body, and string concatenation with += inside a loop. The response includes a finding for each issue, with the matched pattern ID, severity, line range, and a suggestion for how to fix it. The confidence scores typically range from 0.7 to 0.95 depending on how closely the code matches the stored patterns.

After enough reviews accumulate, you can use MongoDB aggregation pipelines to answer questions like "what issues keep showing up?" across all submissions. Aggregation pipelines work by passing documents through a series of stages, where each stage performs an operation like filtering, grouping, or sorting. The output of one stage becomes the input for the next.

Create an AnalyticsService with three pipelines that surface different views of your review data.

The first pipeline groups findings by category and counts how many times each category appears. This tells you where a team's code most often needs improvement:

public List<CategoryCount> getCategoryCounts() {
    Aggregation aggregation = Aggregation.newAggregation(
            Aggregation.group("category").count().as("count"),
            Aggregation.sort(Sort.Direction.DESC, "count")
    );
    return mongoTemplate.aggregate(aggregation, "review_findings", CategoryCount.class)
            .getMappedResults();
}

Aggregation.group("category") is a $group stage that collects all findings with the same category value into one group. .count().as("count") adds a field called count to each group that holds the number of documents in it. Aggregation.sort(Sort.Direction.DESC, "count") orders the groups so the most frequent category appears first. mongoTemplate.aggregate() runs the pipeline against the review_findings collection and maps each result into a CategoryCount object.

The second pipeline uses the same structure but groups by severity instead. This shows the balance of critical, warning, and informational findings across all reviews:

public List<SeverityCount> getSeverityDistribution() {
    Aggregation aggregation = Aggregation.newAggregation(
            Aggregation.group("severity").count().as("count"),
            Aggregation.sort(Sort.Direction.DESC, "count")
    );
    return mongoTemplate.aggregate(aggregation, "review_findings", SeverityCount.class)
            .getMappedResults();
}

If most findings are CRITICAL, the team may need to focus on fundamental practices. If the distribution skews toward INFO, the codebase is generally healthy.

The third pipeline is more involved. It identifies which specific patterns keep recurring across reviews by joining data from two collections. The following diagram shows how documents flow through each stage:

Aggregation pipeline diagram showing the stages from match through group, sort, limit, lookup, unwind, and project

public List<PatternFrequency> getTopPatterns() {
    Aggregation aggregation = Aggregation.newAggregation(
            Aggregation.match(Criteria.where("matchedPatternId").ne(null)),
            Aggregation.group("matchedPatternId").count().as("count"),
            Aggregation.sort(Sort.Direction.DESC, "count"),
            Aggregation.limit(10),
            Aggregation.lookup("review_patterns", "_id", "_id", "pattern"),
            Aggregation.unwind("pattern"),
            Aggregation.project()
                    .and("pattern.name").as("patternName")
                    .and("count").as("count")
    );
    return mongoTemplate.aggregate(aggregation, "review_findings", PatternFrequency.class)
            .getMappedResults();
}

This pipeline has several stages, so here is what each one does:

  • Aggregation.match(Criteria.where("matchedPatternId").ne(null)) filters out findings that have no matched pattern. Not every finding maps to a stored pattern (the LLM can flag issues independently), so this stage removes those before counting.
  • Aggregation.group("matchedPatternId").count().as("count") groups the remaining findings by which pattern they matched and counts the occurrences.
  • Aggregation.sort(Sort.Direction.DESC, "count") orders patterns by how frequently they were matched.
  • Aggregation.limit(10) keeps only the top 10 results.
  • Aggregation.lookup("review_patterns", "_id", "_id", "pattern") performs a join with the review_patterns collection. The _id from the grouped result (which holds the matchedPatternId value) is matched against the _id in the review_patterns collection. The matching document is placed into a new array field called pattern. This is similar to a SQL JOIN, but the result is always an array because MongoDB does not assume a one-to-one relationship.
  • Aggregation.unwind("pattern") flattens that array. Since each grouped result matches exactly one pattern, the pattern array has one element. unwind replaces the array with the single document inside it, which makes the fields easier to access in the next stage.
  • Aggregation.project() selects the final output fields. .and("pattern.name").as("patternName") pulls the name field from the joined pattern document and renames it to patternName.and("count").as("count") keeps the count from the grouping stage. Everything else is excluded from the output.

Expose these three pipelines through an AnalyticsController:

@RestController
@RequestMapping("/api/analytics")
public class AnalyticsController {

    private final AnalyticsService analyticsService;

    public AnalyticsController(AnalyticsService analyticsService) {
        this.analyticsService = analyticsService;
    }

    @GetMapping("/categories")
    public List<CategoryCount> getCategoryCounts() {
        return analyticsService.getCategoryCounts();
    }

    @GetMapping("/severity")
    public List<SeverityCount> getSeverityDistribution() {
        return analyticsService.getSeverityDistribution();
    }

    @GetMapping("/top-patterns")
    public List<PatternFrequency> getTopPatterns() {
        return analyticsService.getTopPatterns();
    }
}

After running several reviews through the system, the category endpoint might return something like:

[
  { "category": "error-handling", "count": 12 },
  { "category": "maintainability", "count": 8 },
  { "category": "security", "count": 5 },
  { "category": "performance", "count": 4 }
]

This tells you that error handling is the most frequent issue category across all reviewed code. These pipelines scan the entire review_findings collection each time they run. For a tutorial with a few dozen reviews, that is fine. In production with thousands of findings, you would want indexes on categoryseverity, and matchedPatternId to speed up the $group stages.

6. Testing the full workflow

Here is the complete flow from start to finish:

Start the application. The DataSeeder loads about 20 patterns and generates their embeddings on first run. You should see the patterns in the review_patterns collection in Atlas.

Add a custom pattern. The library is extensible. Add a pattern that is specific to your codebase:

curl -X POST http://localhost:8080/api/patterns \
  -H "Content-Type: application/json" \
  -d '{
    "id": "logging-user-passwords",
    "name": "Logging user passwords",
    "description": "Writing user passwords to log output in authentication flows",
    "language": "java",
    "severity": "CRITICAL",
    "category": "security",
    "exampleBadCode": "logger.info(\"Login: user={}, pass={}\", username, password);",
    "exampleGoodCode": "logger.info(\"Login attempt: user={}\", username);",
    "explanation": "Passwords in logs violate security policy and compliance requirements."
  }'

Submit code with a known issue. Send a snippet with an obvious anti-pattern:

curl -X POST http://localhost:8080/api/reviews \
  -H "Content-Type: application/json" \
  -d '{
    "code": "public String readConfig() {\n    FileInputStream fis = new FileInputStream(\"app.conf\");\n    byte[] data = fis.readAllBytes();\n    return new String(data);\n}",
    "language": "java"
  }'

The response includes a finding for the unclosed FileInputStream with a matchedPatternId pointing to the "unclosed resources" pattern.

Submit code with a subtler issue. Try a snippet that does not exactly match any stored pattern's example:

curl -X POST http://localhost:8080/api/reviews \
  -H "Content-Type: application/json" \
  -d '{
    "code": "public void backup(Path source, Path dest) throws Exception {\n    BufferedReader reader = Files.newBufferedReader(source);\n    BufferedWriter writer = Files.newBufferedWriter(dest);\n    String line;\n    while ((line = reader.readLine()) != null) {\n        writer.write(line);\n        writer.newLine();\n    }\n}",
    "language": "java"
  }'

Even though this uses BufferedReader and BufferedWriter instead of FileInputStream, the vector search still finds the "unclosed resources" pattern as a top match because the semantic meaning is the same: resources opened without try-with-resources. Check the similarity score in the response to see how closely it matched.

Check analytics. After running a few reviews, hit the analytics endpoints:

curl http://localhost:8080/api/analytics/categories
curl http://localhost:8080/api/analytics/severity
curl http://localhost:8080/api/analytics/top-patterns

These show the accumulated data across all your reviews.

Conclusion

You built a code review assistant with three layers. Atlas Vector Search matches submitted code against the pattern library by semantic similarity, so it finds issues even when the code looks different from the stored examples. Spring AI sends the matched patterns and the code to an LLM, which returns structured findings with severity, line ranges, and fix suggestions. MongoDB aggregation pipelines turn the accumulated findings into trends across submissions.

From here, you could expand the pattern library with anti-patterns from your own team's code reviews. You could add support for reviewing full files or Git diffs instead of snippets, or experiment with code-specific embedding models for better similarity matching. A feedback endpoint where developers mark findings as helpful or not would let you improve pattern quality over time.

The complete source code is available in the companion repository on GitHub.

The post AI-Powered Code Review Assistant: Automated Code Analysis with Spring AI and MongoDB appeared first on foojay.

]]>
https://foojay.io/today/ai-powered-code-review-assistant-automated-code-analysis-with-spring-ai-and-mongodb/feed/ 0
Azul Payara May 2026 Release – What’s New https://foojay.io/today/whats-new-in-the-may-2026-azul-payara-release/ https://foojay.io/today/whats-new-in-the-may-2026-azul-payara-release/#respond Thu, 14 May 2026 11:40:02 +0000 https://foojay.io/?p=123788 Table of Contents A critical security fix, patched across every supported branchAzul Payara Community 7.2026.5Azul Payara 6.38.0: Continued Jakarta EE 10 SupportAzul Payara 5.87.0: Jakarta EE 8 Support ContinuesAzul Payara 4.1.2.191.55: Legacy Branch Still MaintainedLooking AheadUpgrading and Feedback The May ...

The post Azul Payara May 2026 Release – What’s New appeared first on foojay.

]]>

Table of Contents
A critical security fix, patched across every supported branchAzul Payara Community 7.2026.5Azul Payara 6.38.0: Continued Jakarta EE 10 SupportAzul Payara 5.87.0: Jakarta EE 8 Support ContinuesAzul Payara 4.1.2.191.55: Legacy Branch Still MaintainedLooking AheadUpgrading and Feedback


The May 2026 release is the largest Payara milestone since the project's inception. Azul Payara Server 7 and Azul Payara Micro 7 ship as generally available, both certified against Jakarta EE 11. This is the first major Payara product release under the Azul brand, arriving six months after Azul completed its acquisition of Payara in December 2025.

Azul Payara Community 7 (download here), the open-source distribution, was the first implementation of any kind to certify across all three Jakarta EE 11 profiles (Full, Web Profile, Core Profile). Azul Payara Server 7 brings that certification to a commercially supported product with enterprise SLAs, making it the first commercially supported Jakarta EE 11 runtime from a major enterprise application server vendor. Both products ship with MicroProfile 6.1 (Config, Metrics, Health, Fault Tolerance, JWT, OpenAPI, REST Client, Telemetry Tracing). Azul Payara Server 7 holds Final TCK certification across all three profiles:

Profile Azul Payara Server 7 Azul Payara Micro
Full Certified --
Web Profile Certified Certified
Core Profile Certified Certified

No other major enterprise application server vendor holds Final certification across all three profiles at Jakarta EE 11. Oracle WebLogic 15.1.1 sits at Jakarta EE 9.1. IBM WebSphere tWAS is frozen at Java EE 7. Red Hat JBoss EAP ships Jakarta EE 10.

Existing Jakarta EE 10 applications deploy without code changes; the jakarta.* namespace is stable between EE 10 and EE 11, so Azul Payara 6 applications move to Payara 7 by upgrading the runtime, not rewriting the codebase. JDK 21 is the minimum (Docker images ship for JDK 21 and JDK 25, the latest LTS). The same .war runs on both Server and Micro without modification. Jakarta Data, the headline API addition in Jakarta EE 11 introduces the @Repository annotation and a standardized data access layer.

This release also ships a critical security fix across every version: Azul Payara Community 7.2026.5, and Azul Payara 6.38.0, 5.87.0, and 4.1.2.191.55.

A critical security fix, patched across every supported branch

A critical security issue has been addressed across Azul Payara Community 7.2026.5 and Azul Payara 6.38.0, 5.87.0, and 4.1.2.191.55.

The fix lands in Azul Payara branches dating back to 4.1.2. Shipping security patches across the full supported lifecycle, not only the latest major release, is one of the practices that long-running Azul customers rely on; this release is a clear example. Azul is a registered CVE Numbering Authority (CNA) under CISA/DHS oversight, with patches backported to all supported versions on a published monthly schedule.

Azul Payara Community 7.2026.5

Community 7.2026.5 tracks the Payara 7 development line and ships additional fixes ahead of the Enterprise cadence.

Security Fixes

  • Remote attacker can read arbitrary files via unsafe parsing of OpenMQ configuration
  • Restrict access to vulnerable EL expressions

Bug Fixes

  • Fix Admin Console freezing after upgrading from Payara 6 to 7

ImprovementsImprovements

  • Update JaccProviderCompatibilityStartup Service
  • Remove Audit Modules
  • Add warlibs support to redeployment via Admin Console
  • Reduce INFO logging for the Jakarta Data implementation
  • Create new deployment descriptors with deprecated properties removed
  • Fix Jakarta Data @Repository methods not throwing UnsupportedOperationException when no implementation logic can be injected at deploy time

Component Upgrades

Docker JDK images refreshed to 21.0.11 and 25.0.3. Dependency updates for Jakarta Faces, MicroProfile Config, Project Reactor, and other libraries.****

Azul Payara 6.38.0: Continued Jakarta EE 10 Support

Azul Payara 6.38.0 continues the Jakarta EE 10 and MicroProfile 6.1 line for customers who are not yet on Payara 7.

Bug Fixes

  • Fix HTTP 403 Forbidden response on correctly authenticated and authorized calls to protected JAX-RS resources
  • Fix illegal reflective access by org.glassfish.pfl.basic.reflection.Bridge when starting Payara Server in Verbose mode

Improvements

  • Deprecate Audit Modules
  • Remove Yubikey Extension

Component Upgrades

Docker JDK images refreshed for JDK 21, 17, 11, and 8 (21.0.11, 17.0.19, 11.0.31, 8u492). Dependency updates for Mojarra and Project Reactor.

Azul Payara 5.87.0: Jakarta EE 8 Support Continues

Azul Payara 5.87.0 retains the javax. namespace, Jakarta EE 8, and MicroProfile 4.1 platform for customers running long-lived applications that have not yet migrated to the jakarta. namespace.

Bug Fixes

  • Fix illegal reflective access by org.glassfish.pfl.basic.reflection.Bridge when starting Payara Server in Verbose mode
  • Fix OIDC proxy support failing due to incorrect redirect URL comparison

Improvements

  • Deprecate Audit Modules
  • Remove Yubikey Extension

Component Upgrades

Docker JDK images refreshed for JDK 21, 17, 11, and 8 (21.0.11, 17.0.19, 11.0.31, 8u492).

Azul Payara 4.1.2.191.55: Legacy Branch Still Maintained

Azul Payara 4.1.2.191.55 receives security updates and targeted bug fixes for customers still running on the Payara 4 branch.

Bug Fixes

  • Fix Payara failing to start OpenMQ Broker in a separate JVM when using LOCAL mode on JDK 11 or later
  • Fix unclosed streams warnings from OpenMQ

Looking Ahead

With Payara 7 GA, the Azul Payara product line now covers the full enterprise Java surface: the JDK (Azul Zulu, Core and Azul Prime), the full application server (Azul Payara Server), and the cloud-native runtime (Azul Payara Micro). All three ship under one Azul contract with monthly security patches, a long term lifecycle per major release, transparent per-vCore pricing, 24-48 hour bug fix SLAs, and 2-hour critical incident response with dedicated support engineers.

Azul Payara 6, 5, and 4 continue to receive monthly security and bug-fix releases on the published schedule. Migration assessments to Azul Payara 7 are available through your Azul account team for customers planning the move.

Upgrading and Feedback

We recommend upgrading to your version’s latest release in this cycle. A critical security patch is available across every supported branch, so there is no reason to delay the upgrade based on the major-version line you run.

For detailed upgrade instructions, see the Payara documentation. To report issues, contribute fixes, or follow the Payara 7 roadmap, visit the Payara GitHub repository. For commercial support, your Azul account team.

Happy deploying!

The post Azul Payara May 2026 Release – What’s New appeared first on foojay.

]]>
https://foojay.io/today/whats-new-in-the-may-2026-azul-payara-release/feed/ 0
BoxLang AI Series: Complete Guide to Building AI Agents https://foojay.io/today/boxlang-ai-series-complete-guide-to-building-ai-agents/ https://foojay.io/today/boxlang-ai-series-complete-guide-to-building-ai-agents/#respond Thu, 14 May 2026 09:26:48 +0000 https://foojay.io/?p=123758 Table of Contents Start Here: A Practical OverviewThe Full SeriesWhat You’ll LearnKey ResourcesWhy BoxLang AIReady to Start Building? The world of AI development is moving fast, but building real, production-ready AI agents doesn’t have to be complex. This series walks ...

The post BoxLang AI Series: Complete Guide to Building AI Agents appeared first on foojay.

]]>

Table of Contents
Start Here: A Practical OverviewThe Full SeriesWhat You’ll LearnKey ResourcesWhy BoxLang AIReady to Start Building?


The world of AI development is moving fast, but building real, production-ready AI agents doesn’t have to be complex.

This series walks you step by step through how to design, build, and deploy AI agents using BoxLang AI. Whether you’re exploring AI for the first time or looking to modernize your current applications, these guides will help you move from concept to implementation with clarity.

Start Here: A Practical Overview

If you’re new to BoxLang AI or want to understand what’s possible before diving into the technical details, start here:

https://foojay.io/today/how-to-develop-ai-agents-using-boxlang-ai-a-practical-guide/

This guide provides a high-level view of how to build AI agents, integrate multiple models, and design real-world workflows using BoxLang.

The Full Series

Follow the series in order to go from fundamentals to advanced implementations:

What You’ll Learn

Across this series, you’ll learn how to:

  • Build AI agents with memory, tools, and reasoning capabilities
  • Connect to multiple AI providers with a single unified API
  • Implement Retrieval-Augmented Generation (RAG) pipelines
  • Work with vector databases and document ingestion
  • Design scalable, production-ready AI workflows
  • Deploy AI agents in modern cloud environments

Key Resources

To help you go deeper and start building right away:

Why BoxLang AI

BoxLang AI is designed to remove the complexity of working with multiple AI providers and tools. With a single API, you can build powerful AI-driven applications without vendor lock-in, while maintaining full control over your architecture.

If you’re working with legacy systems, BoxLang also allows you to introduce AI capabilities incrementally without needing a full rewrite.

Ready to Start Building?

Explore the series, try the examples, and start building your own AI agents today.

If you have questions or want to see how this can apply to your existing systems, feel free to reach out to the Ortus team.

Next →

The post BoxLang AI Series: Complete Guide to Building AI Agents appeared first on foojay.

]]>
https://foojay.io/today/boxlang-ai-series-complete-guide-to-building-ai-agents/feed/ 0