Why TeaQL Java Can Run from Android to the Cloud: JPMS, Runtime Contracts, and SQL Adapters
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
| Piece | Responsibility in Android |
|---|---|
| Generated domain library | Entities, typed query vocabulary, expressions, and metadata |
teaql-core | Shared entity, request, context, policy, and metadata contracts |
teaql-runtime | Runtime composition and common execution behavior |
| Portable SQL and SQL data-service layers | Translate domain data operations into the SQL execution path |
| SQLite dialect | Database-specific SQL and schema behavior |
teaql-android | Connect execution and transactions to native Android SQLite |
| Android application | UI, 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
| Environment | Example | What it illustrates |
|---|---|---|
| Android | Robot task board | Local business operations using native SQLite, without a backend network dependency |
| Desktop | Compose Desktop vending machine | A Kotlin desktop interface consuming a generated Java domain library |
| Terminal | World Cup CLI | A standalone command-line application backed by embedded SQLite |
| Web service | Spring Boot vending machine | Integration with Spring lifecycle and JDBC infrastructure |
| Web service | Quarkus vending machine | Framework-specific startup and injection around a JDBC-backed domain runtime |
| Web service | Micronaut vending machine | Another 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.
| Concern | Android or local application | Server application |
|---|---|---|
| Physical storage | Native SQLite or embedded JDBC database | JDBC data source and selected database dialect |
| Runtime lifecycle | Activity/application or standalone process | Container startup and request lifecycle |
| Execution scheduling | Background work coordinated with UI lifecycle | Request workers and connection-pool limits |
| Identity and policy | Locally established execution context | Server-authenticated context and authorization |
| Presentation | Native screen, Compose UI, or terminal | HTTP API and framework controllers |
| Transactions | Provider-local database transaction | Provider-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.
