Java / Spring Boot Standard
*Status:* v1.1 authoritative · Owner: Backend team (CTO sign-off) · Last reviewed: 2026-07-03 · Review cadence: quarterly
The standard for FT backend teams (Java 21+/Spring Boot).
It is self-contained — a backend engineer needs only this page (plus the shared FE↔BE contract).
Frontend engineers have their own Angular standard.
This page is the visual form
of the authoritative ft-java-standards skill; on conflict with any other Java standard, this wins.
|
Two standards, two teams, one platform. They diverge only where the stacks genuinely differ (e.g. the interface naming prefix) and they share exactly one thing: the wire contract. |
J1 · Packages = the catalog of the code ⭐
The single idea most people miss at first: a package is a folder, and the package tree is how you catalog and find code. You locate a class by walking feature → role, never by scrolling one giant package. So where a new file goes is a design decision, not an afterthought.
The rule: a new type goes into the sub-package that matches its role (handler, service,
repository, mapper, entity, config…). If that sub-package doesn’t exist, create it.
Never drop a new class into the module/app root package — the root is not a junk drawer.
| ✅ Do — feature first, role second | ❌ Don’t — dumped in the root |
|---|---|
|
|
| New role in a feature? Add the sub-package. New feature? Add the feature package, then its role sub-packages. The tree should read like a table of contents. |
J2 · Naming — role suffix, no Hungarian
| Rule | Java |
|---|---|
Role suffix |
|
Enums |
end with |
Interfaces |
no |
Variables/fields |
|
Methods |
verb phrases ( |
Constants |
|
Forbidden |
❌ |
|
"Hungarian notation" = baking a variable’s type or scope into its name via a prefix, instead of letting the type system carry that. Name by meaning, not type:
|
The I-prefix divergence is intentional — Java stays idiomatic (no prefix), TS keeps it. The
Angular standard explains its side.
|
The role suffixes (…Enum, …Properties, …Service) are a deliberate FT convention for
greppability and uniform cataloguing — not a claim that suffixing a type’s name is idiomatic Java. Where
idiom and convention diverge (e.g. enums), the convention wins on these pages.
|
J3 · Immutability by default
| ✅ record DTO | ❌ mutable DTO |
|---|---|
|
|
DTOs are record`s — or, for a large DTO that needs a builder, an immutable Lombok `@Value
@Builder (records have no builder). On JPA entities use only @Getter/@Setter (they need hydration) —
never @Data / @EqualsAndHashCode / @ToString on an entity: generated equals/hashCode/toString
over all fields break on lazy associations, generated ids, and bidirectional cycles. @Data is never used on a DTO (it’s mutable).
J4 · Constructor injection only
| ✅ Do | ❌ Don’t |
|---|---|
|
|
J5 · Structured, greppable logs — one file, grep by module & device ⭐ (no MDC)
Everything can go to one log file — as long as you can grep it by module and by
device / session id. We do that with explicit key=value fields on each line.
We do not use MDC.
Why not MDC? It is thread-bound and FT flows are not.
MDC stores context in a thread-local. FT requests hop across thread pools, Hazelcast queues, MQTT
listeners, and CoAP/CWMP sessions — the thread that finishes the work is rarely the one that started
it, so MDC context silently vanishes mid-flow. Explicit ids carried on the session/command object
survive every hop.
| ✅ Do — explicit key=value | ❌ Don’t |
|---|---|
|
|
| Field | Meaning |
|---|---|
|
the CPE serial — primary grep key; on every device-scoped line |
|
when a session / transaction exists |
module |
the logger name ( |
Use SLF4J parameterized logging (or 2.x fluent addKeyValue for JSON sinks). A small helper can
render the deviceId=… sessionId=… prefix consistently.
|
| Level | When |
|---|---|
|
an operation failed and needs attention (context + cause). Log once, at the owning boundary — not log-and-rethrow at every layer. Not for handled/expected outcomes. |
|
recoverable / degraded — retry, fallback, deprecated path, slow response. |
|
significant, low-cardinality business events (device registered, task completed). ❌ never per-message / per-loop. |
|
developer detail (payloads, decisions); off in prod by default. |
|
very fine, per-iteration. |
On a firehose (MQTT/CoAP/telemetry) never log at INFO per message — use DEBUG/TRACE, aggregate
(processed N in T ms), or throttle/sample. Guard with log.isDebugEnabled() only when building the
argument is itself expensive (parameterized logging already defers toString).
|
J6 · Outcomes & building blocks
Outcomes, not exceptions. Handlers return sealed OperationResult / AsyncResult and pattern-match:
return switch (asyncResult) {
case AsyncResult.Success<T>(var v) -> new OperationResult.Success<>(map(v));
case AsyncResult.Error<T>(var e) -> toDeviceError(e);
case AsyncResult.Timeout<T> ignored -> toTimeout();
};
@Slf4j for logging · @UtilityClass for static-only utils · Optional<T> over null · java.time.* for time.
Naming suffix vocabulary
Handler · *Service · *Store (non-JPA → Optional) · *Repository (JPA) · *Dao/*DaoJdbcImpl
· *Mapper · *Resolver · *Validator · *Request/*Response (records) · *Result · *Command ·
*Entity (JPA) · *Properties · *Listener · *Dispatcher · Abstract · *Impl (only with an
explicit interface split).
J7 · Tests & TDD
| Convention | Rule |
|---|---|
Method name |
|
|
unit — mock collaborators |
|
integration — real infra (Testcontainers / in-memory / protocol emulator). ❌ never mock the DB |
Coverage |
new/changed code ≥ 80% line+branch (diff coverage). No global gate — legacy isn’t blocked. |
Test quality |
❌ never mock the subject under test (mock boundaries only: repository, remote client, time) · deterministic — inject |
Assertions |
new test classes use AssertJ ( |
| Cover behaviour and edge cases, not lines — an executed line with no assertion doesn’t count. Don’t write tests purely to move the number. |
Task graduation — pick the workflow
-
Architecture-led (new module/feature, refactor, cross-cutting) → an approved SDD (Spec-Driven Development) before any implementation. The orchestrator writes it from the SDD template and the CTO signs off; only then does coding start.
-
TDD-led (bug fix, business rule, protocol message with a known spec) → write the failing test first (no full SDD).
@Test @DisplayName("writes a single resource value")
void should_write_value_when_path_is_writable() { ... }
J8 · Protocol spec = source of truth
| For any protocol parameter/message work, validate paths & semantics against the authoritative spec before writing code or tests — BBF TR-069 / TR-181 / TR-369, OMA LwM2M. Derive fixtures from the spec’s worked examples. |
Use the bundled data-model XML and the bbf_params / bbf_protocol_specs knowledge base, plus the
cwmp-xml and oma-lwm2m-expert skills.
J9 · Caching
| ✅ Do | ❌ Don’t |
|---|---|
|
|
Configure every IMap with TTL + max-size + eviction. Cache reads, evict on the write path.
Don’t cache the per-CPE telemetry firehose (ClickHouse). A cache is never a source of truth.
|
J10 · Constants & enums
| ✅ Do | ❌ Don’t |
|---|---|
|
|
Shared keys — error codes, header names, protocol paths — are named constants in one place.
J11 · External libraries
-
Prefer the JDK and Spring first, then a library already in the repo. ❌ No second library for a job an existing one does (no two JSON / HTTP / date libs).
-
A new dependency needs approval; pin versions (no
+/latest/ranges); manage centrally (BOM / version catalog). -
Vulnerability scan is part of "done" when deps change — upgrade when safe, flag HIGH/CRITICAL you can’t bump; ❌ never
--force/ silence the scanner. Respect licenses (no GPL/AGPL shipped without sign-off).
J-DB · Database contract
Customers pick MySQL OR Oracle. No foreign keys. Deliberate indexes. ClickHouse only for QoE. Batch the hot tables.
Naming (tables & columns)
snake_case, singular table names (cpe, cpe_async_task); no reserved words. FK columns end id
(a plain app-enforced id); booleans is/has_; timestamps at/_date; indexes <table><cols>_idx.
Keep identifiers portable and short — Oracle allows 128 chars since 12.2, but legacy schemas cap at 30.
Audit columns (every business table)
created / creator and updated / updator — created/updated are timestamps (insert / every
update), creator/updator record the actor. Populate via Spring Data JPA auditing
(@CreatedDate/@CreatedBy/@LastModifiedDate/@LastModifiedBy + AuditingEntityListener) or in the
JDBC upsert. (Append-only ClickHouse time-series is exempt — created is the partition/sort key.)
Create-or-update = atomic upsert (not read-then-write)
| ✅ Do — one atomic statement | ❌ Don’t — SELECT then branch |
|---|---|
|
|
Keep the …MySQL / …Oracle SQL pair (see CpeParameterAutomationDaoJdbcImpl /
CpeEventAutomationDaoJdbcImpl). Needs a unique key/constraint to match on.
|
Portable MySQL/Oracle (Liquibase)
<property name="number" value="int(11) unsigned" dbms="mysql"/>
<property name="number" value="number(11)" dbms="oracle"/>
<property name="now" value="now()" dbms="mysql"/>
<property name="now" value="sysdate" dbms="oracle"/>
<changeSet id="seq" author="x" dbms="oracle">
<createSequence sequenceName="CPE_PARAMETER_0"/>
</changeSet>
| Concept | MySQL | Oracle |
|---|---|---|
boolean |
|
|
text |
|
|
auto id |
|
sequence |
paging |
|
|
No foreign keys · deliberate indexes
| ✅ Do | ❌ Don’t |
|---|---|
|
|
| No DB-level FK means referential integrity is app-enforced — guard inserts, clean up orphans in code/jobs, and never let a missing parent corrupt a flow. The trade is portability, not "don’t care". |
ClickHouse = QoE / periodic only
CREATE TABLE cpe_data (serial String, name_id UInt32, value String, created DateTime)
ENGINE = MergeTree
PARTITION BY toDate(created)
ORDER BY (serial, name_id, created)
SETTINGS index_granularity = 8192;
-- current-state: ReplacingMergeTree(version) + is_deleted
Batch the overloaded tables
jdbc.batchUpdate(sql, rows, BATCH_SIZE, (ps, row) -> { ... });
// relational ~1000 (+ order_inserts, rewriteBatchedStatements)
// ClickHouse ~100 · drain queues with drainTo + bulk, re-queue on failure
Default persistence = Spring Data JPA (relational); raw JdbcTemplate for ClickHouse & hot paths.
Don’t grow new hand-rolled *DaoJdbcImpl.
|
Transactions · N+1 · pagination
-
@Transactionalon the service layer — not controllers, not repositories;readOnly = trueon query paths. Self-invocation is not proxied (a@Transactionalmethod called from the same class runs without a transaction) — split the bean.REQUIRES_NEWonly deliberately. -
❌ No repository/remote call inside a loop (N+1) — batch it (
INquery, fetch join /@EntityGraph, projection, batch fetch size). Verify hot queries by count (Hibernate statistics or a query counter in an*IT), not by eyeballing. -
Every new listing endpoint/query over a growing table is bounded —
Pageableor an explicit limit. An unboundedfindAll()on a growing table is a defect, not a style choice.
J12 · HTTP APIs — two styles, documented, validated
FT has two API surfaces — match the one you’re in, don’t mix. Full contracts: Internal API (UI↔BE) · External API (REST).
| Internal FE↔BE | Northbound / public / integration | |
|---|---|---|
Style |
POST-RPC |
REST — |
Verbs |
|
|
DTOs |
records |
|
Errors |
HTTP |
external → real HTTP status + FT code in body · internal s2s → |
Error catalog |
internal |
separate |
Versioning |
n/a |
none — additive-only ( |
Reference |
oneiot-ui / iotw (Internal contract) |
ft-provision-api · ft-northbound-api (External contract) |
-
Document every public endpoint with springdoc OpenAPI —
@Tag(controller),@Operation(method),@Schema(fields). A public API without OpenAPI is incomplete. -
Validate at the boundary —
@Valid/@Validated+jakarta.validation(@NotNull/@NotBlank/@Size) on the request DTO. -
One global
@RestControllerAdviceper service maps exceptions → the error shape above. ❌ No per-controller try/catch; ❌ never leak stack traces.
J13 · Observability
-
Metrics via Micrometer + Actuator, scraped by Prometheus; custom metrics through
MeterRegistry/@Timed. -
Low-cardinality tags only — ❌ never a per-serial tag (cardinality blow-up); the serial belongs in logs (J5).
-
Metrics = rates / latency / counts; logs = events with
deviceId/sessionId.
J14 · API security (external surfaces)
-
External APIs: Spring Security
SecurityFilterChain+ JWT (jjwt);@PreAuthorizefor method-level rules. -
Stateless: CSRF disabled, no server session; auth/authorization failures →
401/403via the global handler. -
❌ Never log tokens/secrets; signing keys live in config/secrets, not code.
J15 · Method & class discipline — code that stays readable ⭐
Review norms, not CI gates: exceeding one needs a stated reason in the PR, not silence.
| Threshold | Rule |
|---|---|
Method ≤ 40 lines |
linear no-branch method may reach ~60 with a reason |
Class ≤ 250 lines / ≤ 20 public methods |
split by responsibility |
≤ 4 parameters |
more → a parameter object ( |
Nesting ≤ 3 |
flatten with guard clauses / early return — happy path unindented |
-
One level of abstraction per method — don’t mix SQL/HTTP mechanics with business flow; extract the mechanics.
-
Rule of Three — DRY is about knowledge, not lines: 2 duplicates → leave + mark
// DUPE:; the 3rd → extract. Look-alike blocks that change for different reasons are not duplicates. A wrong abstraction costs more than duplication. -
YAGNI applies to capabilities, never to quality — no "future-proof" params or config knobs without a consumer; but never an excuse to skip tests/error handling/cleanup.
-
❌ No speculative interface+impl — a single-implementation
@Servicegets no interface (Mockito mocks classes fine); add it when the second implementation arrives.*Implonly with a real split (J6 vocabulary). -
Composition over inheritance; closed hierarchies =
sealed+ records (J6). -
Comments say WHY (constraint, protocol quirk, trade-off) — never narrate what the next line does. ❌ No tutorial comments, no commented-out code in commits.
Simplicity first (KISS)
Solve it the simplest way that passes the tests and reveals intent:
-
Lambdas and method references over anonymous classes —
items.forEach(this::process),Comparator.comparing(Device::serial); ❌ nonew Comparator<>() { … }boilerplate. A lambda ≤ ~3 lines; longer → extract a named private method and reference it. -
The smallest construct that works: a method beats a class; a class beats a hierarchy; a hierarchy beats a framework. ❌ No patterns for their own sake — no factory/strategy/builder/ wrapper where a constructor or direct call does the job; a pattern earns its complexity with a real, present need (Rule of Three, not a hunch).
-
❌ No cleverness: nested ternaries, one-liners that need a comment to decode, reflection or generics gymnastics where plain code works. If the reviewer has to pause — too clever.
-
No premature optimization: write the clear version first; complicate only with a measurement in hand (J18 / perf).
J16 · Exceptions & error flow
-
Handlers return outcomes, not exceptions — sealed
OperationResult/AsyncResult(J6); exceptions are for boundary precondition violations and truly exceptional states. -
❌ Never swallow: no empty
catch, no blanketcatch (Exception e)over a method body — catch the narrowest type you can actually handle; the rest propagates. -
Rethrow with cause — never drop the original exception; never log and rethrow (one owner per failure — J5).
-
Fail fast at the boundary (
@Valid,Objects.requireNonNull(x, "msg")) so deep code assumes sane state. -
❌ No exceptions for control flow — expected outcomes (not-found on a lookup) belong in the result type /
Optional.
J17 · Modern Java idioms
-
Streams for collection transformations (
map/filter/collect) — don’t default to imperative loops out of habit; imperativeforis right for side-effect loops and complex early termination. ❌ No side effects inmap/filter. -
Optional— return type only: ❌ never a field, parameter, or collection element; ❌ no.get()— useorElse/orElseThrow/map/ifPresent. -
varwhen the type is obvious from the right-hand side; never in signatures. Text blocks for multi-line SQL/JSON/XML. -
Time =
java.time.*only (❌java.util.Date/Calendar); logic that reads "now" takes an injectedClock(J7 testability). -
Closed sets =
sealedinterface + pattern-matchingswitch— exhaustive, nodefaultthat hides a missed case (J6).
J18 · Concurrency & async
-
Every remote call has explicit timeouts (connect + read) — no library-default infinities; a hot-path sync call also needs a deliberate failure strategy (retry with backoff / circuit breaker / fallback).
-
❌ Never block indefinitely on a request thread — no
.get()/.join()without a timeout; composeCompletableFuturestages (thenApply/thenCompose/exceptionally). -
Virtual threads (Java 21+ repos): ❌ no
synchronizedaround I/O (pins the carrier) —ReentrantLock; auditThreadLocal(context travels on objects — J5). -
Shared state: immutability first (J3); else
java.util.concurrentcollections/atomics — ❌ no hand-rolledsynchronizedpatterns. -
@Scheduled/ bulk jobs: protect against overlap (fixedDelay, or a DB lock /FOR UPDATE SKIP LOCKEDwith multiple instances); make jobs idempotent — they will rerun.
J19 · Security baseline (code-level)
| Violations here are blocking review findings, not style notes. API-surface rules — J14. |
-
SQL: parameterized only — JPA named params /
JdbcTemplate?. ❌ Never concatenate a value into SQL — includingORDER BY/column names from input (whitelist those). -
XML/SOAP parsing — XXE off, everywhere (TR-069/CWMP is XML — this is our attack surface):
disallow-doctype-decl, external entities off, no external DTD — on every factory (DocumentBuilderFactory, SAX, StAX,TransformerFactory, JAXB source). -
❌ No Java deserialization of untrusted data (
ObjectInputStream.readObject) — JSON/Protobuf instead; Jackson polymorphic typing only with an explicit allowlist validator. -
Secrets never in code, config defaults, logs, URLs, or exception messages; rotatable without a code change.
-
Security-meaningful randomness (tokens, nonces) =
SecureRandom— ❌ neverjava.util.Random/Math.random(). -
Files from user input: resolve against a base dir +
normalize()+ verifystartsWith(base). -
Bind requests to DTO records (J3), never to an
@Entity— mass assignment. -
OS commands:
ProcessBuilderwith an argument list — ❌ never a shell string with input concatenated in.
✅ Review checklist
-
New type in a role sub-package, not the root
-
DTO is a record (or immutable
@Value+@Builderif large);@Datanever on a DTO,@Getter/@Setteronly on JPA entities -
Constructor injection; no field
@Autowired -
Logs carry
deviceId/sessionIdaskey=value; no MDC; module = logger name; right level, no INFO on hot paths -
Tests
should_X_when_Y+@DisplayName;ITon *real infra; new code ≥ 80% diff coverage -
No magic numbers/strings; fixed sets are
enum`s; tunables in `@ConfigurationProperties -
Caching on Hazelcast (
IMap, TTL+eviction) — no@Cacheable/SpringCacheManager, no hand-rolledMap -
New dependency justified + pinned + vuln-scanned; no duplicate-purpose library
-
HTTP API: right surface (FE↔BE POST-RPC vs northbound REST); OpenAPI; request
@Valid; one global@RestControllerAdvice(errors by layer) -
Metrics via Micrometer/Actuator (low-cardinality tags); external APIs secured (Spring Security + JWT,
@PreAuthorize) -
Protocol paths validated against the spec first
-
DB: portable, no FK,
snake_casesingular names, audit cols (created/creator/updated/updator), atomic upsert (no select-then-write), named index, batch hot tables -
DB use:
@Transactionalat the service layer (readOnlyreads); no call-in-loop N+1; new listings paginated/bounded -
No
I-prefix, noFT-prefix, no Hungarian;camelCasevars, verb methods; enums end…Enum -
Size discipline: method ≤ 40 lines, ≤ 4 params (else a record), nesting ≤ 3; no speculative interface+impl; comments say why; no commented-out code
-
Simplicity: lambdas/method references; the smallest construct that works; no patterns without a present need; no clever one-liners; no premature optimization
-
Exceptions: none swallowed; narrowest catch; rethrow with cause; results-not-exceptions in handlers
-
Idioms: streams for transforms;
Optionalreturn-only, no.get();java.time+ injectedClock -
Concurrency: remote calls have timeouts; no blocking
.get()/.join()on request paths; nosynchronizedaround I/O;@Scheduledoverlap-safe + idempotent -
Security baseline: parameterized SQL only; XXE off on every XML/SOAP parser; no untrusted Java deserialization;
SecureRandom; DTO binding (never@Entity)
| Next → the Angular standard · the shared FE↔BE contract. |