Declarative Data Service Routing for Polyglot Persistence
In modern complex application architectures, polyglot persistence has almost become a standard configuration. Within a single system, core transactional data may require PostgreSQL for strong consistency guarantees, massive execution logs might need Meilisearch for full-text search, state information could be thrown into Redis for ultra-high-speed caching, and local development might even downgrade to rusqlite.
The trade-off is coordination complexity: connection pools, provider SDKs, and routing decisions can leak into application code.
1. The Cost of Provider-Specific Branching
If you implement such polyglot persistence using traditional architectures, your business code will typically be littered with scenarios like this:
// Provider-specific routing embedded in business logic
if entity.is_task() {
pg_pool.execute("INSERT INTO task...", &task).await?;
} else if entity.is_execution_log() {
meilisearch_client.index("task_execution_log").add_documents(&log).await?;
}
This approach requires multiple connection pools and provider SDKs, while routing and synchronization logic become mixed with the business workflow.
The same leakage also increases the surface area a coding agent must understand. It can copy the wrong provider API or mix database-specific assumptions into a workflow. Moving an entity from one provider to another can then require changes throughout the application layer.
TeaQL addresses this by making the storage destination part of reviewed model metadata.
2. TeaQL's Solution: The "Underscore Philosophy" and "Inheritance Mechanism"
In the TeaQL framework, we adhere strictly to the philosophy of "Model as the Single Source of Truth". How do we elegantly resolve routing across multiple data services? We compress all the complexity into the system core, leaving only a simple multiple-choice question for developers (or AI) in the XML model file.
Let's look at how a TeaQL model defines the destination of data storage:
<!-- 1. Global configuration: Define the overall data_service on the root node. The entire domain defaults to rusqlite. -->
<root name="robot-kanban-service" org="doublechaintech" data_service="rusqlite">
<!-- 2. Default inheritance: No data service declared here; automatically inherits rusqlite from root to go to relational storage. -->
<task
name="string()"
status="string()"
/>
<!-- 3. Local override: Business requires logs to support full-text search, so we explicitly switch to meilisearch via _data_service. -->
<task_execution_log
action="string()"
detail="string()"
_data_service="meilisearch"
/>
</root>
Why the "Underscore"?
Hidden here is a core syntax design of the TeaQL parser: distinguishing business data fields from framework metadata.
- Attributes without an underscore (such as
name="string()"ordata_serviceon the root node) are treated by the parser as business-level entity data fields (Fields). - Attributes prefixed with an underscore (such as
_data_serviceor_audit_mask_fields) represent metadata (Metadata) that controls framework behavior.
If we wrote data_service="meilisearch" without the underscore on <task_execution_log>, the parser would assume that the task has a business field named data_service! Through this ultra-minimal underscore convention, we achieve complete decoupling of business and framework configuration without breaking the purity of the XML at all.
3. Transparent Dispatching Under the Hood
When the generator scans the model above, it reads _data_service and passes the context to the STG templates. The final entity code generated on the Rust side looks like this:
#[derive(Clone, Debug, PartialEq, TeaqlEntity)]
// The macro is injected with data_service = "meilisearch"
#[teaql(entity = "TaskExecutionLog", table = "task_execution_log_data", data_service = "meilisearch")]
pub struct TaskExecutionLog { ... }
No matter where the underlying data resides, business code uses the same generated domain API.
// Business code: Saving a task automatically routes to rusqlite under the hood
let mut task = Task::new("Deploy App");
task
.audit_as("Create a deployment task")
.save(&ctx)
.await?;
// Business code: Saving a log automatically invokes the Meilisearch Provider SDK under the hood
let mut log = TaskExecutionLog::new("System start");
log
.audit_as("Record the task execution start")
.save(&ctx)
.await?;
The generated entity metadata and provider traits route each audited save to the configured data service. Business code does not choose a connection pool or call a provider SDK directly.
4. Reducing Cognitive Load
Under an architecture like TeaQL:
- Fewer routing mistakes: Business developers do not select a database connection pool for each entity.
- A smaller agent-facing API: Coding agents can work through the generated domain API instead of composing several provider SDKs.
This separates an architectural decision from daily business coding. During modeling, an architect reviews the _data_service destination. During implementation, generated metadata keeps provider dispatch consistent with that decision.
The result is not the removal of polyglot-persistence complexity. It is a clearer boundary: provider-specific behavior remains in the runtime and provider implementations, while business workflows depend on generated domain contracts.
