Skip to main content

Dynamic Fields: Runtime-Extensible Entity Properties in TeaQL 1.523

· 4 min read
Philip Z
Architect

Starting with TeaQL 1.523-RELEASE, we are shipping a long-awaited capability: Dynamic Fields — a structured, type-safe, auditable way to attach custom properties to any entity at runtime, without touching the core domain model.

The Problem

In real-world enterprise systems, the core domain model can never anticipate every field a customer will need. Consider these scenarios:

  • SaaS multi-tenancy: Customer A needs a "customer asset number" field on Platform; Customer B doesn't.
  • Plugin systems: A plugin wants to tag Order entities with a priority score.
  • Field incubation: A new field isn't mature enough to promote into the standard model, but the business needs it now.

The traditional approaches — adding nullable columns to the schema, or dumping everything into a JSON blob — each have serious drawbacks. Nullable columns pollute the model; JSON blobs lose type safety and queryability.

The Solution: Controlled Dynamic Fields

TeaQL Dynamic Fields sits between these extremes. Every dynamic field has:

  • A registered definition — field code, display name, data type (STRING, NUMBER, BOOL), and lifecycle status.
  • Scoping — fields can be global or scoped to a tenant/environment.
  • Type enforcement — you declare the type at registration; reads and writes are type-checked.
  • Audit integration — every write carries purpose and comment, flowing into the standard TeaQL audit trail.

This is not raw key-value storage. It's a controlled extension mechanism with the same rigor as your compiled domain model.

Architecture

The feature ships as two new modules plus bridge points in teaql-core:

teaql-dynamic-fields-api          ← API contracts & in-memory provider
├── DynamicFieldsFacade ← Fluent read/write API
├── DynamicFieldsProvider ← SPI for storage backends
├── InMemoryDynamicFieldsProvider ← Demo/test implementation
└── 20 value objects ← FieldDef, FieldValue, Selection, etc.

teaql-dynamic-fields-jdbc ← JDBC persistent provider
├── JdbcDynamicFieldsProvider ← Works with any JDBC DataSource
└── DynamicFieldsSchema ← Auto-creates 2 tables via ensureSchema()

teaql-core ← Bridge points
├── UserContext.dynamicFields() ← Access via context
├── Entity.dynamicFields() ← Read values on any entity
└── SearchRequest.selectDynamicFieldsWith(...)

The API module has zero external dependencies — it depends only on teaql-core. The JDBC module reuses your existing DataSource and auto-creates its internal tables (teaql_dynamic_field_def and teaql_dynamic_field_value) on first use.

Quick Start

1. Add the Dependency

<dependency>
<groupId>io.teaql</groupId>
<artifactId>teaql-dynamic-fields-jdbc</artifactId>
<version>1.523-RELEASE</version>
</dependency>

2. Initialize the Provider

// Reuse your existing DataSource (MySQL, PostgreSQL, SQLite, etc.)
JdbcDynamicFieldsProvider provider = new JdbcDynamicFieldsProvider(dataSource);
provider.ensureSchema(); // auto-creates tables if not exist

3. Register a Field Definition

DynamicFieldDef def = new DynamicFieldDef();
def.setScope(DynamicFieldScope.global());
def.setOwnerType("School");
def.setCode("short_name");
def.setName("School Abbreviation");
def.setDataType(DynamicDataType.STRING);
def.setStatus(DynamicFieldStatus.ACTIVE);
provider.registerFieldDef(ctx, def);

4. Read and Write via the Fluent Facade

DefaultDynamicFieldsFacade df = new DefaultDynamicFieldsFacade(provider);

// Write
df.withContext(ctx)
.purpose("Initialize school data")
.comment("Set abbreviation")
.owner("School", 1001L)
.string("short_name").set("MIT");

// Read
String name = df.withContext(ctx)
.owner("School", 1001L)
.string("short_name").get();
// → "MIT"

5. Bulk Read

DynamicFieldValues values = provider.loadValues(ctx, owner,
new DynamicFieldSelection()
.selectString("short_name")
.selectNumber("student_capacity")
.selectBool("is_key_school"));

String name = values.getString("short_name");
int capacity = values.getNumber("student_capacity").intValue();
boolean isKey = values.getBool("is_key_school");

Supported Data Types

TypeJava TypeUse Case
STRINGStringLabels, codes, identifiers
NUMBERNumber (Long/Double)Scores, quantities, amounts
BOOLBooleanFeature flags, toggles

More types (dates, enums, references) are planned for future releases.

Storage Backend Options

ProviderPersistenceUse Case
InMemoryDynamicFieldsProviderNone (JVM lifetime)Unit tests, demos
JdbcDynamicFieldsProviderAny JDBC DataSourceProduction use

The JDBC provider has been tested with SQLite, PostgreSQL, MySQL, and H2. It uses standard SQL and should work with any JDBC-compliant database.

Design Principles

  1. No schema migration required. Dynamic fields live in their own tables. Your core entity tables are untouched.
  2. Type-safe, not a JSON dump. Every field has a declared type. The API enforces it.
  3. Audit-first. Every mutation carries purpose and comment, integrating with TeaQL's holographic trace logging.
  4. Provider-pluggable. Implement DynamicFieldsProvider to back dynamic fields with Redis, DynamoDB, or any other store.
  5. Zero coupling to core entities. Dynamic fields are keyed by (ownerType, ownerId). No changes to your generated entity classes.

What's Next

  • Query integration: Filter and sort by dynamic field values in SearchRequest.
  • Code generation support: Auto-generate dynamic field read/write helpers from model annotations.
  • Rust parity: Bring dynamic fields to teaql-rs for the Rust runtime.
  • Rich types: Date, Decimal, and entity-reference field types.

Upgrade Guide

To upgrade to 1.523-RELEASE:

<properties>
<teaql.version>1.523-RELEASE</teaql.version>
</properties>

The dynamic fields modules are fully optional — adding them has zero impact on your existing domain model and data service layer.


Dynamic Fields fills a critical gap in model-driven development: the ability to extend the domain at runtime with the same discipline and safety as compile-time modeling. We believe this strikes the right balance between flexibility and control.

Try it out and let us know what you think on GitHub!