


Make an AI Follow the Style and Module Rules of a Large Java Backend
Use when an AI works in a large Java server built from modules and providers. It reads the entry points first, follows the machine-checked code style and the oldest supported Java version, declares every module it uses, checks claims in the source, and tests locally before pushing.
To use this AI rule
## Before Editing, Learn How the Modules and Providers Fit Together
A large server of this kind is assembled from modules, not written as one application. A module declaration names the module and the service interfaces it exposes; a provider implements those services with one specific technology; other modules consume only the declared interfaces. Providers are discovered through the platform service-loader mechanism and follow a fixed lifecycle: a preparation phase that builds objects and reads configuration, then a start phase that activates services.
### Trace the pipeline before touching a stage
Collectors send telemetry over the transport layer, receiver plugins parse and validate it, the analysis engine aggregates it through the project's own domain languages, storage plugins persist the result, and query plugins serve it to the interface. A change in one stage usually needs a counterpart in another.
### Read the entry points first
Before editing, read the core module definition, the module system base class, the grammar files for the domain-specific languages, the application entry point, and the architecture documentation. A name does not tell you how a feature works.
## Follow the Code Style the Build Checks Automatically
Style here is machine-checked, so a violation blocks the build rather than starting a debate. Do not print to standard output; use the project's logging facade. Do not add author annotations. Do not use tab characters, star imports, unused or redundant imports, empty statements, or multiple consecutive blank lines. Keep one statement per line, keep whitespace after commas, semicolons, and casts, and keep whitespace around operators.
### Required patterns
Annotate overridden methods. Override equality and hash together. Give every documentation parameter, return, and exception tag a real description. Write long constants with an uppercase suffix letter. Put the default branch last in a switch. Respect the maximum file length and split anything approaching it.
### Naming
Constants and static variables use upper case with underscores, type parameters use upper case, types use pascal case, and locals, parameters, and members use camel case. Prefer immutable-collection factories and the project's annotation processor over hand-written boilerplate, and prefer final locals and parameters.
### Do not inline fully-qualified class names
Add an import and use the short name in field declarations, method signatures, locals, and generic arguments. The only acceptable exceptions are a genuine simple-name collision between two imported classes, and a single documentation link where the short name would be ambiguous to a reader.
### Do not write one-line delegate methods
A wrapper whose only body forwards to another class adds a hop without value. Inline the call at the use site or call the underlying object directly, including by method reference.
### License headers
Source, markup, and configuration files carry the project's license header. Formats that cannot hold comments are excluded by the license configuration; follow that configuration rather than adding headers by hand where they do not belong.
## Name Classes and Methods by Their Job, Not by a Metaphor
Classes and methods are named for their function, not for an abstract metaphor. Prefer concrete verbs such as load, apply, unregister, compile, verify, commit, and roll back over metaphorical ones such as seed, hydrate, bootstrap, and prime.
### Let the name state the job
A class that loads static rules is named for loading static rules, not for an evocative image. A timer that synchronizes database state on a schedule is named for that synchronization, not for a tick.
### A metaphor is a signal to split
If you cannot name a method without reaching for a metaphor, the method is probably doing too much. Split it until each piece has a plain functional name.
## Write Comments Only Where the Code Can't Explain Itself
Do not explain what the code already states. Comment only where reading the code is not enough: an invariant that is not visible in this method, a cross-cutting ordering constraint, why a non-obvious branch exists, why a seemingly redundant call is required. If the code is clear, write no comment.
### Connect logic that lives apart
A check that exists only because of a guarantee made elsewhere deserves a one-line pointer to that guarantee. A workaround for a known defect elsewhere deserves a link to it.
### Trim restated documentation
Method documentation describes the contract, not the body. Multi-paragraph rationale belongs in the design document, not in the source file.
## Use Framework Assertions and Ask Before Changing Tests
Always use the test framework's assertion methods. Never use the language's built-in assertion statement in a test. Even where the test runner happens to enable those statements, framework assertions are the convention elsewhere in the codebase, survive any runner configuration, and produce a structured failure that names the actual value instead of a context-free error.
### Fix failing tests, do not quietly change them
If existing unit, integration, or end-to-end tests fail, try to fix them. If a test case needs to be modified, or test code needs to be deleted, that change must be double-checked by a human before it is applied: surface the proposed modification or deletion and the reasoning, and wait for explicit approval rather than changing or removing the test yourself.
### Match the test type to the scope
Unit and integration scopes use different patterns and different harnesses. Prove behavior at the scope where it actually lives rather than duplicating the same assertion at every level.
## Write Code That Compiles on the Oldest Supported Java Version
A project that supports several long-term-support releases must compile and run on the oldest one, even when the build machine has a newer compiler. Assume the minimum, not what your editor accepts.
### Constructs to avoid when the minimum is older
Arrow-form switch expressions, the stream terminal that returns a list directly, text blocks, record types, pattern matching in a type check, and sealed types are all newer than an older baseline. Use a traditional switch with explicit cases and breaks, a collector to build the list, string concatenation, a regular class with generated accessors, an explicit cast after the type check, and a regular class or interface instead.
### Constructs that are safe
Immutable collection factories, the newer optional methods, lambda expressions, method references, and the stream interface itself are available on an older baseline and need no workaround.
### Verify by searching, not by trusting the build
Search the source tree for each forbidden construct and confirm no matches before you claim compatibility. A build on a newer compiler will not catch these for you.
## Keep Submodules and Generated Code in Sync With the Build
Protocol definitions and the web interface may live in submodules. Always clone with submodules included, and update them explicitly when a protocol change requires it. A protocol change that is not reflected in the submodule pointer will not build for anyone else.
### Generate before you analyze
Run the compile step before analyzing generated code, and install the project so the precompiler and generated classes are exported before running tests. Never hand-edit generated classes; change the definition that produces them and rebuild.
### Rebuild the whole project after cross-module changes
If you changed more than two modules, or pulled or rebased from the remote, build the whole project rather than selecting individual modules. Incremental single-module builds can leave stale archives in the local repository or the library directory when the archive size does not change but the content does, which produces runtime failures that are very hard to trace.
## Keep the Docs and Changelog Accurate and Displaying Correctly
Documentation is rendered by a markdown engine, so consider how it will look, not only how the source reads. Relative file paths are valid and work both in the repository and on the documentation site; keep using them rather than absolute links. Update the changelog whenever behavior changes.
### Keep each changelog bullet on one line
Do not hard-wrap a sentence across source lines inside a bullet; nested sub-bullets are fine because they are real list items. The changelog becomes the release body, and the release renderer turns a single continuing newline into a line break, so wrapped bullets show jagged mid-sentence breaks on the release page even though the documentation site reflows and hides the problem.
### Freeze a design proposal once it ships
When the implementation of a design proposal lands, synchronize the proposal once to what was actually built: every proposed change written in past tense, with no remaining pending-work, future-work, open-dependency, or empty-until language for anything in its scope. After that synchronization the proposal is frozen. Further metrics, dashboards, and incremental enhancements go into the living operator documentation instead. Only genuinely small optional follow-ups may remain under future work; never a large change.
### Cross-reference with anchor links
Reference another section with a markdown anchor link, never with a section symbol or a bare section number. Verify that every anchor resolves to a real heading under the site's slug rules, because a broken anchor silently renders as plain text.
## Declare Every Module Your Code Looks Up
Every lookup of another module's service must be backed by a declaration. If code asks the module manager to find a module, directly or through a helper, that module's name must appear in the provider's required-modules list. A missing declaration does not fail at boot; it throws the first time that code path fires. Wrapping the call in a broad catch is not a substitute: declare the module, and keep defensive handling only for transient provider outages. When auditing a branch, search every touched module for module-manager lookups and confirm each target is declared.
### Update every implementation, not just the main one
When you add a service to a core module's service list, update all provider implementations of that module, including the mock providers used by standalone tools and test harnesses. Search the tree for subclasses instead of assuming there is one. The main server is not the only packaging; standalone tools boot with mock providers and a subset of modules, so changes to core module contracts must reach them too, or those tools fail at startup.
### Prefer the already-populated service over the coordination module
Receiver and analyzer modules usually do not declare the cluster coordination module, so looking it up directly throws at runtime. Go through the core module's peer-management service, which the coordination layer already populates and which exposes the peer list most components need. If a module genuinely needs coordination primitives, declare that module explicitly.
### Do not route intent through thread-local side channels
Passing a caller's intent through a thread-local that downstream code reads creates invisible coupling between distant code paths, leaks across asynchronous hand-offs such as executors and event loops, and makes behavior impossible to understand from a method signature. Extend the interface instead: add a parameter, a method, or a mode value that appears in the signature, even if it costs more lines. The rare exceptions are context propagation where the whole industry has standardized on a thread-local, and security principals enforced by a framework.
## Wait for Schema Changes to Reach All Nodes Before Writing Data
When schema or metadata changes are broadcast to coordinator nodes and propagated to data nodes asynchronously, a definition call returns as soon as the coordinator accepts it, carrying a revision marker, without waiting for the other nodes. A data write whose definition has not propagated yet is dropped at the executing node, logged and skipped, so a fence is required.
### Wait on the revision watermark
After issuing a definition change, block on the client's schema watcher until every live node reports that it has applied at least the returned revision, bounded by a timeout, before releasing dependent writes. A read-after-write check against the registry does not prove propagation.
### Handle deletes and synchronous stores
When a delete returns no usable revision marker, fall back to the dedicated wait-for-deletion call. Stores whose definition statements are synchronous on the coordinator need no fence at all. Do not reintroduce the older idiom of polling the lookup call until you can read your own write; it predates the barrier interface and has been replaced.
### Check which client library you are reading
Barrier and watcher types may live in an in-tree client fork rather than in the standalone published client. Their absence from the published client is not proof the interface is gone.
## Check the Source and Docs Instead of Guessing
Never guess or speculate. All analysis must be grounded in source code, documentation, or observed behavior. Read the implementation instead of assuming how a feature works from its name or from convention. Read the project documentation, the guidance files inside submodules, and the readme files before proposing a design.
### Verify configuration and flags
Check which flags and environment variables actually exist, what their defaults are, and how they are parsed, including any prefix convention that binds flags to environment variables automatically.
### Read the projects you depend on
Before assuming a capability, read the source and documentation of the dependencies and companion projects involved: deployment charts for supported fields, test infrastructure for supported options, client libraries for available calls. If a companion project is likely checked out locally, ask for the local path, because searching a local clone is far faster than fetching files one at a time from a remote interface.
## Run the Code and Tests Locally Before Pushing to CI
Run the code, start the container, and execute the test locally before pushing. Continuous integration is not a trial-and-error environment.
### Validate paths and directory structures
Check where data actually lands rather than assuming a directory name from a flag name; a root-path option frequently creates a differently named subdirectory beneath it.
### Validate generated or edited configuration
After editing configuration files, especially with stream editors or scripts, validate them with a real parser before committing. Corrupted configuration causes silent failures in automation.
### Confirm images and binaries exist
Verify the container image is actually published before writing pipeline or end-to-end configuration that depends on it, and confirm which binaries, shells, and directories exist inside the image before writing commands that use them. Image availability depends on publishing workflows having completed. Note where images live: first-party project images may be on a code-host registry with full commit identifiers as tags, while official and third-party images are on the public registry with conventional tags.
## Push Through the Project's Review Workflow and Approved Automation Steps
Use the project's dedicated submission workflow for committing and pushing to a review branch. It runs pre-flight checks such as compilation, style checking, and license header verification before every push, and opens the review request if one does not exist yet.
### Respect the automation allow list
The organization may enforce an allow list for third-party automation steps, requiring each one to be pinned to an approved revision identifier rather than a version tag. If a review is blocked because a step is not allowed, consult the approved list and update the workflow files to use the approved pin. Steps owned by the platform vendor or by the organization itself are generally allowed without pinning.
### Say what is missing rather than guessing
If you cannot find the source, do not know how to run something, or the code does not make the answer clear, ask rather than speculate. When investigating runtime behavior, set up a local test with real data and observe the actual output instead of predicting it.
