Skip to content

What a Seasoned Java Developer Would Not Expect

Tentackle looks familiar from a distance — entities, transactions, a desktop client, Maven plugins. Up close, it deviates from mainstream Java frameworks (Spring, JPA/Hibernate, Jakarta EE) in ways that routinely surprise experienced developers. This document is the distilled list of those deviations, each with a pointer to the document that covers it in depth.

The programming model

  • Entities are interfaces, and new does not compile. Every PDO, operation, and most framework components are interfaces; implementations are located via the build-time service registry and instantiated as dynamic proxies: Invoice invoice = on(Invoice.class).
  • Multiple inheritance, emulated. A PDO weaves two separate implementation classes — one for persistence, one for domain logic — into a single object via a proxy. The two concerns never compete for the one extends slot, and neither layer knows the other.
  • The entity model lives in a comment block. Attributes, relations, indexes, and validations are declared in a DSL embedded as a comment in the PDO interface source — a single definition from which the persistence interfaces and implementations, the DDL, and the database migrations are all generated.
  • Generated code is woven into your sources, inside guarded folding regions, by the Wurbelizer — not dumped into target/generated-sources. You read and debug one file per class.
  • Rich domain objects, not anemic ones. Because a PDO is never in a degraded state, behavior lives on the entity (invoice.approve()), not in transaction-script services — see PDOs vs. Traditional ORMs.

No ORM lifecycle

  • There is no LazyInitializationException — the exception type does not exist. A PDO carries its session and context inside itself and can navigate any relation at any time, in any JVM, no matter how many tiers separate it from the database.
  • No entity states. No managed/detached/removed, no persistence context, no merge(), no fetch plans, no Open-Session-In-View, and no DTO layer — the PDO itself travels between JVMs and stays fully functional on arrival.
  • No identity map. A session holds no references to the PDOs loaded through it, so objects are garbage-collected the moment your code drops them — batch loops never need em.clear(). Shared identity is an opt-in, per-entity choice via the PdoCache.
  • Explicit saves. There is no automatic dirty-flush; you call persist() where you mean it. Modification is still tracked per attribute — the write is just where you put it.
  • Transactions demarcate the database, not your objects. A @Transaction commits or rolls back the unit of work; object usability is never tied to its boundary.

Aggregates are enforced, not a convention

  • The persistence layer knows root entities and components. Each component row carries its root's id and type (rootId/rootClassId), so a component knows its aggregate root without a query.
  • A component cannot be persisted on its own. The "set the foreign key and save the child" path that every ORM allows simply has no API; the only door into the database is the root, and persisting the root validates the entire aggregate first — including cross-entity invariants.
  • A component loaded outside its root context is made immutable, and authorization is aggregate-scoped: if you may not read the root, its components behave as if they did not exist.
  • Undo is aggregate-scoped too. A snapshot reverts root attributes, component lists, and modification flags atomically — and a stale snapshot is rejected instead of silently misapplied.

Location transparency and the multi-tier cascade

  • Every JVM is a node, and a server is itself a client. Nodes stack into a cascade of arbitrary depth — client → edge proxy → core server → database — and the same artifact runs unchanged at any position. Whether a session is local JDBC or remote is decided solely by a connection URL; adding or removing a tier is configuration, never code.
  • Caches stack per tier. Reads trickle up until a tier's PdoCache hits; invalidations trickle down via database-backed serial counters — a coherent multi-level cache hierarchy with surgical expiry, no message broker required (modification tracking). CQRS-style read/write splitting falls out of the cascade as a deployment choice.
  • Cursors work remotely. A ResultSetCursor streams a JDBC ResultSet held on the server while a client three tiers away drives it — the batch code cannot tell the difference.
  • The change tracker is deliberately a non-daemon thread. If it dies, the application dies with it: an app that has silently lost its cache-coherency notifications would be wrong quietly, and Tentackle would rather stop.

A wire protocol of its own

  • TRIP replaces RMI and Java serialization with a compact binary format: a type dictionary transmitted once per client, variable-length encoding, deduplication, and sparse-collection optimizations. Remote interfaces are plain Java interfaces — no throws RemoteException, no export/unexport ceremony.
  • SD-WAN and NAT friendly. Unlike RMI, TRIP never sends hosts, IPs, or ports back to the client, so gateways and relays just work — and each new remote proxy costs no extra round-trip.
  • Transports are picked per link by URL scheme: plain TCP, TLS, deflate compression, pre-shared-key encryption (for links without a PKI), and QUIC/RFC-9000 — mixed freely within one cascade.
  • Version skew fails at connect time, not mid-stream. Built-in client/server version checking and an evolution-tolerant type dictionary turn incompatibility into a clear, immediate error.

Correctness is always on

The framework's first commitment is fail loudly and early rather than corrupt quietly:

  • Optimistic locking cannot be turned off. Every write carries the serial the object had when read; a stale write matches zero rows and is rejected — from a form, a batch, or an admin tool alike (locking). An opt-in, auto-expiring token lock adds early conflict warning on top.
  • A @Transaction transparently retries a whole unit of work when the failure was temporary — an optimistic-lock conflict, a deadlock, a serialization failure.
  • Validation travels with the object. Declared on the member, scope-aware (persisting vs. interactive), severity-aware, executed before every persist regardless of code path — and a ValidationResult survives a TRIP round-trip with a path (invoice.lineList[3].price) that a client can map back to the exact widget.
  • Annotations take dynamic parameters. condition = "$isPrinted" or a Groovy expression — CompoundValue evaluates strings into constants, property references, or scripts at runtime, side-stepping Java's compile-time-constant limitation.
  • Objects can be switched read-only at runtime. The Immutable contract guards every generated setter; cached PDOs are shared across threads finally immutable, and freezing cascades along aggregate boundaries. Freezable extends the same idea to mutable JDK value types like java.util.Date.
  • Interceptors are compile-time verified and deterministically ordered — a misplaced @Transaction or @Secured is a compilation error, not a runtime surprise.

Everything resolvable at build time is resolved at build time

  • No classpath scanning, ever. Service discovery is written by the compiler into META-INF text files and looked up from maps at runtime (services). Annotation parameters are captured as data too, so resolving "class id 2001 ⇄ Message" loads no classes at all — startup time does not grow with the model.
  • The same SPI works on the classpath and the module path. Every one of the 37 reactor modules is a real JPMS module, yet applications run modular, non-modular, or mixed.
  • The database schema is derived, migrated, and verified from the model. The SQL plugin generates backend-specific DDL, computes incremental migrations by comparing the model against a live database's metadata, and can fail the build if the two drift.
  • Even resource bundles and validation annotations are build-checked by the check plugin.

A full-stack JavaFX framework

Tentackle is one of the very few frameworks that cover a JavaFX application full-stack — the same programming model reaches from the SQL database through the middle tiers into the desktop UI. The JavaFX ecosystem offers control libraries and themes, but almost nothing that binds a UI to a persistence layer, a security model, i18n, and a deployment story in one piece:

  • An extended FX layer, not a control kit. Zero per-widget glue. Every wrapped control is a first-class, bindable component carrying a model type, a value translator, and mandatory/changeable/error state. Controllers are discovered via the SPI, their FXML/CSS/bundles located by convention, and components bound to model members by name — in the model's own types (BigDecimal, LocalDate, an enum, a PDO), never as raw strings (tentackle-fx).
  • FXML, CSS, and .properties live in src/main/java, co-located with their controllers — copied as resources during the build, so SceneBuilder finds them by relative path.
  • The rich desktop client is generated from the model. The RDC layer bundles everything the UI needs per entity behind one GuiProvider: default editors, finders, context menus, and tables whose columns are binding paths — with layouts that roam with the user via database-backed preferences, plus printing and Excel export.
  • Validation reaches the widget. A server-side ValidationResult maps back to the exact control that caused it (InteractiveError) — authoritative checks on the server, precise feedback on the screen.

The deployment story is equally unusual. The jlink/jpackage plugin builds self-contained images (and native installers with menu entries and icons) — and it differs from other jlink plugins in ways that matter:

  • It packages any Java application — modular, non-modular, or mixed. It debunks the myth that jlink requires full modularization: it analyzes all dependencies (reading module-info, falling back to jdeps) and derives the packaging category itself — real modules go into the trimmed runtime image, automatic modules land next to it on a module path, classpath jars on a classpath. Nothing about your dependency graph has to change first.
  • The generated start script is the other half of the image. Instead of jlink's built-in launcher (which cannot pass a module path or classpath at all), the plugin materializes its dependency analysis into a platform-specific launch script generated from editable Freemarker templates — keeping platform details out of the POMs, which stay minimal because the Maven model already provides most of the configuration.
  • Self-update is built in. A per-user image can replace itself in place via the update service: the server alone dictates the target version, the download URL, and the update script, so bulk downloads can be steered to a CDN or local mirror instead of through the application server — the successor to the retired Java Web Start.

Batteries a mainstream stack does not include

  • Authorization rules are data, not code. The security subsystem stores ACLs as ordinary PDOs, editable at runtime through a UI, and enforces them inside select/persist/delete — a row you may not read is never handed to you.
  • Translations live in the database. tentackle-i18n transparently intercepts bundle loading; translators edit texts live with BundleMonkey — effective immediately in every running JVM, including locales the developers never shipped.
  • Preferences roam. A java.util.prefs-style API backed by the database, so window geometry and table layouts follow the user to any workstation.
  • Business numbers are pooled, transactionally. Number sources hand out invoice/order numbers from persistent pools — and take them back.
  • Secrets are never casually readable. A single application-specific Cryptor encrypts passwords in filtered resources at build time, in properties files, in memory, and on the wire.

The trade, stated honestly

None of this is free: more exceptions by design, explicit saves, a cohesive but smaller ecosystem, and machinery you don't need if you control your topology and run a conventional web stack. Where Tentackle earns its keep is spelled out in Why Tentackle Fits Technical and Scientific Applications — and where a mainstream framework is the better fit is stated there too.