TeaQL Showcase: See What Your Business Code Actually Does
Instead of hiding database behavior behind an opaque ORM, this demo shows the full execution path of a domain action:
Instead of hiding database behavior behind an opaque ORM, this demo shows the full execution path of a domain action:
We are thrilled to announce the release of TeaQL 1.0.0! This milestone marks the stabilization of our core APIs, major performance improvements, and a completely redefined model-driven development experience across both Rust and Java (Spring Boot) ecosystems.
TeaQL 1.0.0 focuses on API Elegance, Developer Ergonomics, AI-Native Workflows, and Transaction Safety.
In a multi-tenant business system, the dangerous query is often not obviously dangerous.
A developer writes a useful request:
Q.candidates()
.selectName()
.selectEmail()
.filterBySkill("Java")
.page(1, 20)
.executeForList(ctx);
The request looks typed, generated, and harmless. But in a platform like Multi Talent, a candidate belongs to a customer account, a workspace, a recruiter team, and often a legal region. A useful query becomes unsafe if it can see outside those boundaries.
That is why TeaQL treats request execution as a runtime boundary, not just a method call.
Imagine Multi Talent as a SaaS platform for recruiting agencies and enterprise hiring teams.
The same TeaQL model may include:
The query language should stay expressive. A team should be able to search candidates by skill, location, availability, interview status, and related job opening.
But every query must also respect infrastructure and customer boundaries:
Those rules should not be copied into every controller.
One option is to add filters wherever a query is written:
Q.candidates()
.filterByCustomer(ctx.currentCustomer())
.filterByRecruiterTeam(ctx.currentTeam())
.filterBySkill("Java")
.executeForList(ctx);
This works until one path forgets the filter. It also asks every feature author to understand every infrastructure and data-protection rule.
Another option is to hide the query behind service methods. That can work for some workflows, but TeaQL deliberately gives teams a generated request language. The runtime should make that language safe instead of forcing teams to abandon it.
TeaQL Java runtime exposes a dedicated UserContext extension point for this
final step:
protected <T extends Entity> SearchRequest<T> enforceRequestPolicy(SearchRequest<T> request) {
return request;
}
Every normal request execution path goes through this hook before the request is submitted to the repository:
SearchRequest
-> UserContext
-> enforceRequestPolicy(...)
-> Repository
-> database provider
That placement matters. It is late enough to see the actual request that is about to execute, but still early enough to change it or reject it.
TeaQL Rust has the same runtime-boundary idea through RequestPolicy on
UserContext. The Rust hook is platform-scoped and runs after entity-scoped
repository behavior, so it can make the final decision before the request
reaches the provider.
A Multi Talent project can extend its generated context and enforce the policy once:
public class MultiTalentUserContext extends MultiTalentGeneratedUserContext {
@Override
protected <T extends Entity> SearchRequest<T> enforceRequestPolicy(SearchRequest<T> request) {
request = super.enforceRequestPolicy(request);
String type = request.getTypeName();
if ("Candidate".equals(type) || "TalentProfile".equals(type)) {
request.appendSearchCriteria(
request.createBasicSearchCriteria(
"customerAccount",
Operator.EQUAL,
currentCustomerAccount()));
request.appendSearchCriteria(
request.createBasicSearchCriteria(
"recruiterTeam",
Operator.IN,
visibleRecruiterTeams()));
}
if ("ResumeAttachment".equals(type)) {
enforceRegionalAccess(type);
}
rejectDangerousRequestShape(request);
auditSensitiveRead(request);
return request;
}
}
The example is intentionally centralized. Feature code can still express the business query:
Q.candidates()
.selectName()
.selectEmail()
.filterBySkill("Java")
.page(1, 20)
.executeForList(ctx);
The runtime adds the customer and team boundaries before the repository sees the request.
The hook is not only about tenant IDs.
In a real platform, request policy can protect infrastructure as well as data:
rawSql for normal users;unlimited() on high-volume entities;For example:
private void rejectDangerousRequestShape(SearchRequest<?> request) {
if (request.getRawSql() != null && !currentUserCanUseRawSql()) {
throw new AccessDeniedException("Raw SQL is not allowed for this user");
}
Slice slice = request.getSlice();
if (slice != null && slice.getSize() > 200) {
slice.setSize(200);
}
}
This is infrastructure protection. It prevents one feature path from turning into a full-table scan, an accidental data export, or an expensive cross-tenant aggregation.
UserContext already knows the runtime facts needed to enforce policy:
Repositories should not need to know every business permission model. Controllers should not need to repeat low-level safety rules. The generated request API should remain readable.
UserContext is the convergence point.
Because the hook sees the final request, it is also a useful place to audit sensitive reads:
private void auditSensitiveRead(SearchRequest<?> request) {
if (!"Candidate".equals(request.getTypeName())) {
return;
}
auditTrail().recordRead(
traceId(),
currentCustomerAccountId(),
currentUserId(),
request.getTypeName(),
request.comment(),
getClientIp());
}
This does not replace business-level audit records such as "approved offer" or "revoked recruiter access." It complements them by making sensitive data access observable at the runtime boundary.
Generated APIs should make business intent visible.
Runtime policy should make that intent safe to execute.
In Multi Talent, a query for candidates should read like a query for candidates. It should not be filled with repeated customer, team, residency, audit, and infrastructure rules. Those rules belong at the boundary where a request is submitted to the runtime.
That is what enforceRequestPolicy is for.
In Rust, the same idea is expressed as a RequestPolicy:
use teaql_core::{Expr, SelectQuery};
use teaql_runtime::{RequestPolicy, RuntimeError, UserContext};
pub struct MultiTalentPolicy;
impl RequestPolicy for MultiTalentPolicy {
fn enforce_select(
&self,
ctx: &UserContext,
query: &mut SelectQuery,
) -> Result<(), RuntimeError> {
if matches!(query.entity.as_str(), "Candidate" | "TalentProfile") {
let customer_id = ctx
.get_named_resource::<u64>("customer_account_id")
.copied()
.ok_or_else(|| RuntimeError::Policy("missing customer account".to_owned()))?;
let tenant_filter = Expr::eq("customer_account_id", customer_id);
query.filter = Some(match query.filter.take() {
Some(existing) => existing.and_expr(tenant_filter),
None => tenant_filter,
});
}
if query.raw_sql.is_some() {
return Err(RuntimeError::Policy(
"raw SQL is not allowed for normal users".to_owned(),
));
}
Ok(())
}
}
Register it during runtime assembly:
let ctx = teaql_runtime::UserContext::new()
.with_module(multi_talent::module_with_behaviors_and_checkers())
.with_request_policy(MultiTalentPolicy);
Java uses UserContext.enforceRequestPolicy. Rust uses RequestPolicy. The
design principle is the same: TeaQL projects get a final, explicit place to
protect the platform and the customer's data before a request reaches the
repository.
Domain-driven development breaks down when the domain language disappears from the code path.
That happens easily in data-heavy systems. The model may be discussed in design sessions, but implementation code becomes SQL strings, mapper files, repository methods, DTOs, and service glue.
TeaQL uses generated APIs to keep the domain model visible.
TeaQL starts from a domain model:
The generator turns that model into APIs. Application code then works with the generated vocabulary instead of reconstructing it by hand.
Generated query methods make intent explicit:
Q.orders()
.filterByMerchant(ctx.getMerchant())
.selectCustomer(Q.customers().selectName())
.selectLineItemList(Q.lineItems().selectSku().selectQuantity())
.countLineItems()
.executeForList(ctx);
The code names the domain structure:
That is more reviewable than a service method that hides several mapper calls and response transformations.
DDD systems are rarely flat. Relations carry meaning:
TeaQL relation loading APIs give those relationships a generated shape. The runtime can still decide how to execute the load efficiently.
Domain changes often arrive as object graphs:
TeaQL Rust models this through graph nodes, graph operation state, mutation planning, and transaction boundaries. Java TeaQL carries the same high-level idea through entity save and graph-oriented runtime behavior.
Domain logic is not only data shape. It also includes:
TeaQL keeps those concerns near runtime context and generated model hooks instead of scattering them across controllers.
Handwritten domain APIs are possible. The problem is consistency.
Generation gives every entity the same baseline:
Teams can then focus on the parts of the domain that are actually special.
Generated APIs are not a shortcut around domain modeling.
They are a way to make the domain model executable, reviewable, and reusable across application code, runtime providers, and AI coding tools.
It is tempting to describe TeaQL as an ORM because TeaQL knows about entities, relations, repositories, and databases.
That description is incomplete.
TeaQL is a generated business API layer. Persistence is one part of the system, but the main value is the generated domain language that sits above persistence.
Most ORM discussions focus on mapping:
Those are real problems. TeaQL also needs to solve them. But large business systems have another repeated problem: the same business request is rebuilt again and again in controllers, repositories, DTOs, SQL, validators, and frontend response logic.
TeaQL optimizes the business API surface.
It generates request APIs that can express:
The generated API is meant to be read by backend engineers, domain engineers, reviewers, and AI coding tools.
An ORM might make it easy to load an Order.
TeaQL aims to make it clear how a complete order page is assembled:
Q.orders()
.filterByMerchant(ctx.getMerchant())
.selectCustomer(Q.customers().selectName())
.selectLineItemList(Q.lineItems().selectSku().selectQuantity())
.countLineItems()
.orderByCreateTimeDescending()
.page(1, 20)
.executeForList(ctx);
The key is not whether this compiles to SQL. It does. The key is that the API names the business shape before the runtime turns it into storage operations.
TeaQL also treats runtime behavior as part of the model:
That is why TeaQL has a runtime layer instead of only a mapper layer.
AI tools should not need to guess SQL, join rules, table names, tenant filters, and response shapes from scattered code.
Generated APIs give AI tools a deterministic vocabulary:
That is a different goal from a traditional ORM.
TeaQL uses persistence mapping, but it is not defined by persistence mapping.
It is a generated business API platform that keeps domain intent visible while allowing the runtime provider to change underneath.
MyBatis is a productive and familiar tool for Java teams. It gives developers direct control over SQL and mapping. For many simple services, that is enough.
The pressure appears when a business page needs more than one table and more than one query shape.
Consider an order page.
A typical order page may need:
With mapper-oriented persistence, this often becomes several mapper methods, XML fragments, DTO assembly, and post-query stitching.
A MyBatis implementation might involve:
That is explicit and controllable, but the business intent is spread across files.
TeaQL tries to express the page as one generated business request:
User userOrderInfo = Q.users()
.filterWithId(userId)
.countOrder()
.facetByOrderStatus("statusWithCount", Q.orderStatus().countOrders())
.selectOrderList(
Q.ordersWithId()
.selectOrderId()
.selectDate()
.offset(0, 10)
.selectLineItemList(
Q.lineItemsWithId().selectImageURL().limit(3)
)
.countLineItems()
)
.execute(context);
The API is still explicit, but the explicitness is at the business level:
TeaQL does not eliminate thinking. It reduces repetitive plumbing:
The generated model becomes the shared vocabulary.
MyBatis remains a good choice when:
TeaQL and MyBatis do not need to be ideological opposites. TeaQL is valuable when the business model is large enough that generated APIs reduce repeated work.
In a TeaQL code review, the reviewer can ask:
That is a higher-level review than checking XML fragments and DTO stitching.
MyBatis gives direct SQL control. TeaQL gives generated business APIs.
For complex business pages, TeaQL keeps page intent closer to the domain model.
TeaQL is a generated business API platform for systems where the domain model is more important than the storage plumbing around it.
Instead of asking developers to repeatedly write repositories, SQL fragments, DTO stitching, relation loading code, and validation glue, TeaQL starts from a domain model and generates APIs that read like business intent.
The current positioning is simple:
Generated Business APIs. Java-proven. Rust-powered. Multi-database ready.
TeaQL separates the business API from the runtime provider.
Domain Model
-> TeaQL Generator
-> Generated Q API
-> Runtime Provider
-> MySQL / PostgreSQL / SQLite / Memory / Edge / Agent
Application code should talk to generated APIs. Runtime code should decide how those APIs execute against a database, memory repository, embedded store, or service runtime.
Depending on the target stack, TeaQL can generate:
In Java, this appears as fluent generated request APIs. In Rust, generated crates expose Q::entities() style query builders over teaql-rs.
Large business systems repeat the same concepts across many screens and services:
Without a generated business API layer, those concepts spread across SQL, mapper XML, repositories, service methods, DTOs, validators, and frontend-specific response shaping.
TeaQL keeps the model vocabulary visible in the code path.
Java TeaQL is the mature, proven path for enterprise systems and Spring Boot services.
Rust TeaQL is the runtime direction for generated domain APIs across PostgreSQL, MySQL, SQLite, embedded SQLite, and memory-backed tests.
The two stacks are not identical, and they do not need to be. They share the programming model: generated APIs over a domain model, executed through a runtime boundary.
User userOrderInfo = Q.users()
.filterWithId(userId)
.countOrder()
.statsFromOrder("statusWithCount", Q.orders().count().groupByOrderStatus())
.selectOrderList(
Q.ordersWithId()
.selectOrderId()
.selectDate()
.offset(0, 10)
.countLineItems()
)
.execute(context);
This is not just a query. It is the shape of a business page expressed through generated APIs.
TeaQL turns domain models into stable business APIs that humans and AI tools can read, compose, and run across Java, Rust, and multiple database providers.
AI coding tools can produce useful application code quickly. The problem is not speed. The problem is boundary control.
If an AI tool has to infer persistence behavior from scattered SQL, repositories, mapper XML, DTOs, and service conventions, it will eventually guess wrong.
TeaQL exists to make that boundary deterministic.
When AI writes data-access code directly, it must infer:
Some of these guesses can pass a simple test and still be wrong in production.
TeaQL generates APIs from the domain model. That means AI code can compose with named business methods instead of inventing storage behavior.
let orders = Q::orders()
.select_customer_with(Q::customers().select_name())
.select_line_item_list_with(Q::line_items().select_sku())
.which_statuses_are("PAID")
.page(1, 20)
.execute_for_list(&ctx)
.await?;
The model controls what fields and relations exist. The runtime controls how the query executes. The AI composes within those boundaries.
A deterministic API can still be expressive:
The point is that the API surface is stable and reviewable.
TeaQL also makes prompts smaller.
Instead of giving an AI tool the entire schema, SQL examples, repository conventions, and DTO rules, a team can provide a generated API guide:
Use TeaQL Q APIs for reads.
Use generated relation selectors.
Execute through UserContext.
Do not write raw SQL unless explicitly requested.
Use graph save for parent-child persistence.
That is a stronger contract than a long explanation of database structure.
The generated API is only half the story. Runtime boundaries matter too.
TeaQL keeps execution behind context and provider layers:
UserContext as the runtime boundary.teaql_runtime::UserContext, repository registries, behavior hooks, and provider registration.AI-generated application code should not own those decisions.
AI tools are fast at composition. They are unreliable at reconstructing business rules from infrastructure code.
TeaQL gives them deterministic business APIs to compose.
TeaQL officially broke away from its legacy brand to become an independent open-source project.
- com.doublechaintech.data
+ io.teaql.data
All classes were migrated, including BaseEntity, BaseRequest, SQLRepository, UserContext, and the entire expression parser framework.
This migration marked the birth of TeaQL as a standalone brand.
TeaQL did not appear from nowhere, and it is not simply a rename of an old generator.
It came from a long engineering lineage around one persistent problem: business software repeats the same structures across projects. Domain models, relationships, permissions, queries, validation, workflows, persistence rules, and presentation metadata appear again and again. The hard question is not whether code can be generated. The hard question is where the generation boundary should live.
The earliest internal version of this direction was built in 2003.
That first version was already focused on reducing repetitive business application code in enterprise systems. The visible Git history of web-code-generator starts on April 1, 2016, but that commit should be read as a preserved snapshot of an older internal lineage, not as the beginning of the idea.
This history matters because the core problem stayed consistent for more than two decades. What changed was the shape of the generated output.
The early web-code-generator approach generated source code directly into application workspaces.
That made sense for the development model of its time. A team could describe domain objects and generate a working application surface:
This proved that model-driven generation could cover much more than CRUD. It could generate a large part of the repetitive structure around real business systems.
But generating source code into the developer workspace also has a cost. Generated files become mixed with handwritten files. Reviews get noisy. Upgrades create large diffs. AI coding agents must search through many repeated files before finding the business logic that actually matters. DevOps teams have to govern generated behavior indirectly through each application repository.
After 2022, web-code-generator became less of a standalone product story and more of the engineering transition path into TeaQL.
Some important work still happened in the old repository during this overlap period:
These were not just old-template maintenance tasks. They helped validate the capabilities that TeaQL later moved behind a cleaner library and runtime boundary.
Modern TeaQL moved the generation boundary.
Instead of scattering generated source files across application workspaces, TeaQL focuses on generating versioned libraries, deterministic business APIs, query DSLs, object graph persistence behavior, and runtime capabilities that applications consume as artifacts.
That difference is important.
The old model was:
domain model -> generated source files inside the application workspace
The TeaQL model is:
domain model -> generated library/runtime artifact -> application dependency
The generated output becomes something that can be tested, published, versioned, upgraded, rolled back, and shared through normal release pipelines.
AI-assisted coding changes the cost of generated source.
If thousands of generated files live beside handwritten business code, the AI agent has to navigate a noisy workspace. It may spend context on repeated scaffolding instead of the business workflow, integration, test, or product behavior that needs attention.
TeaQL's newer boundary gives AI agents a cleaner surface:
This does not remove generation. It makes generation more governable.
The same boundary helps DevOps.
Generated capabilities can move through normal engineering controls:
That is much cleaner than repeatedly regenerating large source trees inside many application repositories.
The older web-code-generator articles should be read as capability history.
They document how the system learned to generate persistence code, service code, frontend pages, mobile clients, forms, validation, search, aggregation, and runtime helpers. Some technologies mentioned there, such as JSP, DVA, older Android and Swift templates, or Taro miniapp generation, are no longer the current recommended stack.
Their lasting value is not the specific old framework. Their lasting value is the engineering lesson: complex business software has repeatable structures, and those structures should be generated from a domain model.
TeaQL is the AI-era productization of that lesson.
This is where TeaQL began. In November 2022, the core runtime shipped with 95 files and 4,636 lines of code.
SQLExpressionParser, PropertyParser, and RawSqlParser form the foundation of type-safe query expressions:
Q.orders().filter(
Q.orders().customer().city().eq("Shanghai")
).executeForList(ctx);
SubQueryParser (62 lines) enables nested queries expressed naturally in Java:
Q.orders().filter(
Q.orders().customer().city().eq("Shanghai")
).executeForList(ctx);
SimpleAggregation supports count, sum, avg, and more directly from the domain model.
SQLRepository as the base with database-specific extensionsThese patterns remain the foundation of TeaQL today.