Skip to main content

Why TeaQL Java Can Run from Android to the Cloud: JPMS, Runtime Contracts, and SQL Adapters

· 18 min read
TeaQL Team
Core Team

TeaQL Java can support an Android application with local SQLite, a Kotlin desktop application, a standalone CLI, and a Java web service because its architecture separates the domain contract from the application container and physical database execution. The key is the direction of dependencies: business operations depend on runtime interfaces, while platform integrations implement those interfaces.

Java's standard module system, JPMS, makes part of that separation explicit and enforceable on the JVM module path. Generated metadata reduces the need for reflective discovery. Execution contexts carry policy and capabilities. SQL adapters connect the common runtime to different database APIs.

The Java application examples show the application forms. This article examines the mechanisms that make them possible, and distinguishes current runtime architecture from older example versions.

JPMS makes the dependency boundary a Java contract​

Splitting a project into Maven artifacts helps package dependencies, but Java packages alone do not express which packages another artifact may access. TeaQL also declares named modules in module-info.java. A descriptor records required modules, exported packages, and consumed or provided services, as defined by the Java module descriptor API.

For example, these are excerpts from TeaQL's current descriptors:

module io.teaql.core {
requires io.teaql.utils;
requires transitive io.teaql.data.dynamic;
requires com.fasterxml.jackson.databind;

exports io.teaql.core;
exports io.teaql.core.meta;
exports io.teaql.core.spi;
// Other public contract packages are exported in the full descriptor.
uses io.teaql.core.spi.ContextAssembler;
}
module io.teaql.provider.jdbc {
requires io.teaql.core;
requires io.teaql.dataservice.sql;
requires java.sql;
exports io.teaql.provider.jdbc;
}

The important observation is what the core requires: utility abstractions, metadata-related capabilities, and Jackson, with no Spring or Android dependency. The JDBC provider depends on the core and SQL contracts; the core does not depend on that provider. A platform integration can therefore change without making its container APIs prerequisites for the domain contract.

On the module path, requires determines module readability, and exports controls access to public types in a package. requires transitive deliberately propagates readability to consumers where a dependency is part of the contract. Packages outside the export list remain encapsulated under normal modular access rules. This gives dependency mistakes a compiler or module-resolution boundary instead of relying only on code-review conventions.

That guarantee has a scope. It applies to named-module use, and can be weakened by explicit access overrides. Putting the same artifacts on the classpath does not retain the same JPMS encapsulation. Also, an exported package is an actual public surface: its contents do not become private simply because the project calls them infrastructure.

See the full core descriptor and JDBC provider descriptor.

Framework independence comes from the runtime contracts​

JPMS describes the boundary; the API design makes the boundary useful. TeaQLRuntime receives metadata, a data-service registry, policy, ID generation, and logging through its constructor. Its generated-module installation path registers metadata, checkers, and schema bootstraps. None of those operations needs a Spring application context.

A query enters the runtime with a UserContext and a typed SearchRequest. In the inspected list-query path, the runtime checks purpose and materialization limits, invokes RequestPolicy.enforceSelect, records trace information, and then runs the resolved data operation. These concerns live above the physical driver.

Consequently, an Android screen and a server endpoint can use the same categories of execution contract even though they create contexts differently. The screen owns interaction and local lifecycle; the endpoint owns authenticated request handling. Both can supply the runtime with the identity, policy, and capabilities appropriate to their environment.

This does not mean a device context is trusted by a remote service. A server must establish its own authenticated context and authorization. Reusing contract types reduces API variance; it does not transfer authority across a network.

Source: TeaQLRuntime.

Standard Java SPI supports composition outside a container​

TeaQL's core declares uses ContextAssembler; the runtime declares a provider:

provides io.teaql.core.spi.ContextAssembler
with io.teaql.runtime.boot.CoreAssembler;

TeaQLUserContextFactory calls ServiceLoader.load(ContextAssembler.class), sorts discovered assemblers by priority, initializes their global resources, and calls mountTo(context) when creating a context. The runtime also includes META-INF/services registration for classpath service discovery.

This provides a standard Java composition path for extensions without requiring Spring scanning, CDI, or Micronaut injection in the core. Container integration can remain an outer adapter around runtime assembly.

The distinction between a mechanism and a configured application matters here. The inspected CoreAssembler mounts demonstration attributes, and the factory constructs DefaultUserContext(null). SPI discovery alone does not establish a fully configured production runtime. An application or extension must still supply the required runtime services and resources.

Sources: context factory and runtime descriptor.

SQL semantics and physical execution have separate boundaries​

Changing a database dialect and changing a platform driver are different tasks. A dialect controls SQL syntax and database-specific behavior. An execution adapter controls how SQL, parameters, results, and transactions pass through a driver API. TeaQL separates these concerns through portable SQL and data-service modules.

Generated entities and requests
|
UserContext / runtime
|
SQL data-service execution
|
SqlExecutionAdapter
/ \
JDBC provider Android SQLite adapter
DataSource SQLiteDatabase

The portable TeaQLDatabase interface describes operations using SQL strings, positional argument arrays, rows, batches, transaction callbacks, and schema metadata. Its basic operations do not require a DataSource, Spring template, or Android database handle in the contract.

The higher SQL execution layer also has a SqlExecutionAdapter boundary. JdbcSqlExecutor implements it using DataSource, Connection, PreparedStatement, and ResultSet. AndroidSQLiteExecutionAdapter implements it using SQLiteDatabase, Cursor, and SQLiteStatement.

For Android, positional reads use rawQuery; writes bind parameters into a compiled statement; batch writes run inside a native SQLite transaction. AndroidSqliteDataServiceExecutor supplies a transaction callback implementation using beginTransaction, setTransactionSuccessful, and endTransaction. It also bridges native execution into a TeaQLDatabase for portable schema work.

That is the technical reason the business API need not know whether a row came from a JDBC result set or an Android cursor. Driver details stay behind the execution boundary, while domain metadata and query construction remain above it.

The abstraction does not make every provider equivalent. The current Android adapter explicitly rejects named-parameter and streaming query methods. JDBC also has unimplemented named-parameter overloads. Provider selection must follow the operations actually exercised by the application.

Nor is the portable SQL module free of Java platform dependencies: its descriptor requires java.sql. Keeping Spring JDBC out of an execution path is a narrower and more accurate claim than saying the entire module has no JDBC-related types.

Sources: TeaQLDatabase, JDBC executor, and Android execution adapter.

Generated metadata reduces dependence on reflective discovery​

A generic persistence layer often discovers classes, constructors, and properties at runtime. TeaQL can make those decisions explicit in domain metadata. EntityDescriptor stores an entity supplier, and its createEntity() method invokes that supplier. Missing registration fails explicitly.

The portable repository uses descriptor-based entity construction in row mapping, including referenced objects. A generated registration can therefore provide a constructor such as Task::new instead of requiring the mapper to discover and invoke it reflectively.

This is helpful alongside JPMS because reflective access to encapsulated packages can otherwise require additional opening of packages. Explicit construction also makes the persistence path easier to inspect when evaluating Android packaging or native-image reachability.

It is a reduction in reflection requirements for those paths, not proof that all application serialization is reflection-free or that a native executable has passed verification. Custom values and integrations require their own analysis.

Sources: EntityDescriptor and portable repository.

How JPMS relates to Android​

Android applications in this collection do not execute as JPMS module-path applications. The TeaQL Android descriptor explicitly documents that downstream Android Gradle builds do not use the Java module system. Its requires static android declaration permits compile-time use of the SDK stub in TeaQL's modular Maven build; it does not supply Android classes on a normal JVM.

JPMS helps TeaQL maintain an explicit dependency graph in its Java build. Android portability then depends on the selected library APIs, bytecode and language support, desugaring, packaging, and native database integration. These must be verified by the Android toolchain and a device or emulator.

Thus the two mechanisms reinforce each other: module contracts make platform coupling visible, and platform adapters provide compatible implementations. JPMS alone cannot make arbitrary JVM code run on Android.

Source: Android module descriptor.

How an Android application consumes the modules​

The task-board example makes the distinction concrete. TeaQL is built as Java modules upstream, but the Android app consumes its artifacts as Gradle dependencies and packages the required code through the Android build pipeline. It does not start the app with java --module-path, and it does not load JPMS modules at runtime.

1. Gradle selects artifacts and compiles the generated domain code​

The example's app/build.gradle declares these dependencies:

// Historical example coordinates, not current installation instructions.
implementation ('io.teaql:teaql-sql:1.500-RELEASE') {
exclude group: 'org.springframework'
}
implementation 'io.teaql:teaql-android:1.500-RELEASE'
implementation ('io.teaql:teaql-sqlite:1.500-RELEASE') {
exclude group: 'org.springframework'
}

It also adds the generated domain library's Java source directory to the Android application's main source set:

sourceSets {
main {
java.srcDirs += ['../generate-lib/lib/src/main/java']
}
}

This puts generated entities, query builders, expressions, and metadata in the same Android compilation as the application. The framework arrives through artifact dependencies; the domain vocabulary arrives through generated source. A separately published domain artifact is another packaging choice, but this example uses the source-set route.

Its compile options enable Java 17 source and target compatibility and core library desugaring, with desugar_jdk_libs:2.0.3. These are explicit Android build choices, not a consequence of adding module-info.java. They still require verification against the selected Android API level and the Java APIs actually used by the dependency graph.

Excluding Spring dependencies expresses the example's intended local runtime path. It does not transform a Spring-dependent class into Android-compatible code. The selected classes must work without those dependencies, and any updated graph must be checked again.

Source: Android Gradle configuration.

2. Android owns the database; TeaQL receives an execution adapter​

TeaQLManager opens taskboard_teaql.db through a SQLiteOpenHelper, then obtains its native SQLiteDatabase. It assembles generated entity metadata and passes the database handle to AndroidSqliteDataServiceExecutor.

The essential wiring in that older example is:

SimpleEntityMetaFactory metaFactory = new SimpleEntityMetaFactory();
new EntityMetaRegistry().assemble(metaFactory);

SqlDataServiceExecutor dataService =
new AndroidSqliteDataServiceExecutor("default", sqliteDb);

TeaQLRuntime runtime = new TeaQLRuntime.Builder()
.metadata(metaFactory)
.dataService("default", dataService)
.build();

DefaultUserContext ctx = new DefaultUserContext(runtime);

This is manual composition inside an Android application. It does not require Spring injection or the SPI factory discussed earlier. The metadata tells TeaQL what the entities and relationships mean; the named data service tells it where and how to execute their persistence operations; the context connects application calls to that runtime.

The example then registers a UI logging sink, ensures schema, and seeds initial objects. Its direct dataService.ensureSchema(ctx) call belongs to the older runtime API. Current code uses the context-owned context.ensureSchema() lifecycle; an upgrade must adopt the matching initialization contract rather than copying this historical bootstrap unchanged.

Source: TeaQLManager.

3. Business code calls domain APIs, not Android database APIs​

Once the context is wired, the task-board logic reads through the generated query facade. The following excerpt is from its status-change workflow:

BusinessTask task = (BusinessTask) Q.tasks()
.returnType(BusinessTask.class)
.withIdIs(taskId)
.comment("Update status")
.purpose("Drag and drop")
.executeForOne(ctx);

if (!E.task(task).isNull()) {
task.transitStatus(newStatusCode);
task.auditAs("Moved task status from drag-and-drop").save(ctx);
}

TaskLogic works with domain objects, query intent, and mutation intent. It does not open a cursor or bind a SQLite statement. The native database handle remains in the infrastructure wiring and adapter.

In the current adapter implementation, physical execution eventually reaches SQLiteDatabase.rawQuery for positional reads or a compiled SQLiteStatement for writes. Results return through row mapping into domain entities. Transaction callbacks are implemented using the native SQLite transaction lifecycle.

This is how Android uses the modular architecture: it selects reusable contracts and implementations as dependencies, supplies a platform adapter, and calls the resulting runtime through generated business APIs. JPMS helps maintain those boundaries upstream; Android's build and runtime execute the selected code through their own mechanisms.

Source: TaskLogic.

4. Module selection has concrete responsibilities​

PieceResponsibility in Android
Generated domain libraryEntities, typed query vocabulary, expressions, and metadata
teaql-coreShared entity, request, context, policy, and metadata contracts
teaql-runtimeRuntime composition and common execution behavior
Portable SQL and SQL data-service layersTranslate domain data operations into the SQL execution path
SQLite dialectDatabase-specific SQL and schema behavior
teaql-androidConnect execution and transactions to native Android SQLite
Android applicationUI, lifecycle, database ownership, scheduling, and context setup

This table describes responsibilities in the current architecture, rather than an exact dependency list for the older 1.500-RELEASE example. In particular, that example's teaql-sql coordinate should not be silently substituted into a current installation guide.

The example stores a single static context. It illustrates the wiring, but a production app must decide context scope, concurrent access, database lifetime, and how work is moved off the UI thread. The architecture provides extension points; those Android lifecycle decisions remain application work.

An application family, from device to service​

EnvironmentExampleWhat it illustrates
AndroidRobot task boardLocal business operations using native SQLite, without a backend network dependency
DesktopCompose Desktop vending machineA Kotlin desktop interface consuming a generated Java domain library
TerminalWorld Cup CLIA standalone command-line application backed by embedded SQLite
Web serviceSpring Boot vending machineIntegration with Spring lifecycle and JDBC infrastructure
Web serviceQuarkus vending machineFramework-specific startup and injection around a JDBC-backed domain runtime
Web serviceMicronaut vending machineAnother framework integration using the same domain development approach

The Android and CLI applications have their own domains. The vending-machine examples provide the closer comparison across desktop and web application forms. This collection demonstrates a shared development approach; it does not establish that every example uses an identical generated library or runtime version.

Android can own local business data​

The robot task board is a local Android application. Its domain is described in a model, and its application code uses generated queries and entity operations to create tasks, load the board, and change task status. A log panel exposes data execution to the user interface.

This matters for applications whose useful work happens on the device. A task board, field checklist, or local inventory tool may need to read and update data without waiting for a server round trip.

TeaQL's Android integration connects database execution to native SQLiteDatabase. The portable SQL boundary lets storage operations be adapted without requiring Spring JDBC in that path. The Android application still owns database lifecycle, threading, screen state, and platform integration.

Local persistence makes local work possible during a network outage. Connecting that work to a remote system adds separate requirements: synchronization, idempotency, authorization, retries, and conflict resolution. The task-board example illustrates local execution, not an automatic device-to-cloud sync engine.

The same development approach works beyond mobile​

The Compose Desktop vending machine uses Kotlin for its interface and a generated Java domain library for business data. The World Cup CLI uses command-line and interactive-shell interfaces over an embedded SQLite store.

Both are useful examples of the runtime boundary. A domain operation need not start inside an HTTP controller or depend on a web framework. An interface can invoke typed business operations through an execution context whether the user clicked a desktop button or entered a terminal command.

The generated vocabulary and explicit execution intent also make business code easier to review. Queries identify what they are reading and why; mutations record the reason for changing state. The surrounding interface can change while these conventions remain recognizable.

What changes when moving from a device to a service​

The reusable layer consists of domain vocabulary, typed data-operation contracts, metadata, and runtime policy interfaces. The platform layer owns the resources and lifecycle required to execute them.

ConcernAndroid or local applicationServer application
Physical storageNative SQLite or embedded JDBC databaseJDBC data source and selected database dialect
Runtime lifecycleActivity/application or standalone processContainer startup and request lifecycle
Execution schedulingBackground work coordinated with UI lifecycleRequest workers and connection-pool limits
Identity and policyLocally established execution contextServer-authenticated context and authorization
PresentationNative screen, Compose UI, or terminalHTTP API and framework controllers
TransactionsProvider-local database transactionProvider-local database transaction

The same transaction interface does not create a transaction across a device and server. Synchronization still needs an application protocol, retry and idempotency rules, and a conflict policy. Likewise, framework portability does not establish Kubernetes readiness or autoscaling behavior.

The feature is valuable because those differences can be handled at explicit boundaries rather than embedded throughout generated domain operations.

What the existing evidence establishes​

The repository's multi-framework test record reports generation, compilation, and application startup for Spring Boot 4.1.0. It reports generation and compilation for Quarkus 3.8.3 and Micronaut 4.3.8, targeting Java 21. Those are different verification levels: compilation alone does not establish database behavior or complete runtime integration.

The Android example's build configuration pins TeaQL 1.500-RELEASE, with minSdk 26, compileSdk 33, and Java 17 source compatibility. These are example coordinates, not a current universal compatibility promise. A modern adoption should verify the selected generator, runtime, Android toolchain, and device combination together.

The architectural analysis uses the local TeaQL Java source tree declaring 1.551-RELEASE; the examples above include older runtime combinations. Descriptor excerpts describe the inspected current source, not modules verified in every example. This article reviews source and recorded results without reporting a new execution campaign. See Latest Versions and Conformance Status for maintained version and capability evidence.

Why this is a TeaQL Java feature​

The useful feature is the combination of model-driven APIs, explicit execution context, query intent, audited mutations, and replaceable runtime infrastructure across several application environments.

A team can use the same way of expressing business data operations in a local Android tool, a desktop application, a CLI, and a Java service. Platform-specific code remains necessary, but it has a clearer boundary around the domain layer. For both human developers and coding agents, that boundary reduces the number of unrelated persistence patterns they must maintain.

The next useful demonstration is a bounded vending-machine workflow executed on desktop and server: compare validation, persisted state, transaction behavior, and audit output using pinned versions. That would extend the existing examples from architectural illustrations into directly comparable portability evidence.