Data Model
The tSM Data Model is a continuously maintained description of the data structures and configuration artifacts in one tSM installation. It brings together information that otherwise lives in separate microservices, public DTOs, Forms, Characteristics, Entity Specifications, Scripts, Listings, Registers, BPMN processes, DMN decisions, and public APIs.
It does not contain business records such as individual customers, orders, or tickets. It describes their shape and their dependencies: which attributes exist, which configuration extends an entity, which script or process uses an entity, and which objects can be affected by a change.
Typical uses include:
- discovering the effective schema of an entity subtype;
- finding all direct users of a Script, Form, Characteristic, or API operation;
- estimating the transitive impact of a planned change;
- navigating from a graph result to the owning tSM configuration;
- exporting the installation model for an offline explorer or analysis;
- identifying incomplete analysis, unresolved references, and stale sources.
An empty usage result means “no usage is known in the current model”. It is not proof that the artifact is unused. Always consider the model status, coverage, source states, and diagnostics together with the result.
Two complementary views
The Data Model exposes one current committed revision through two complementary views. A revision is a lightweight visibility boundary, not a permanently stored historical snapshot.
Schema view
The schema view is intended for code and UI that needs to understand data fields. For an entity subtype it combines:
- the native schema discovered from the microservice Public DTO;
- subtype metadata and relationships discovered from code;
- manually maintained descriptive
EntityTypemetadata where it adds information that code cannot provide; - the selected Entity Specification and its Characteristics;
- the resulting merged JSON Schema, flattened attributes, and references.
For example, Customer/B2B can contain native Customer fields together with installation-specific Characteristics.
The response retains provenance, so a consumer can distinguish native and configured attributes.
Graph view
The graph view represents artifacts as nodes connected by directed, typed edges. An outgoing edge is a dependency of the source node; an incoming edge is a known user of the target node. A bounded incoming traversal therefore answers “what can be affected if this object changes?”.
Every edge records:
- a semantic relation such as
tsm:usesFormortsm:referencesEntityType; - evidence strength (
EXACT,HIGH,MEDIUM, orLOW); - semantics (
DECLARED,STATIC_POTENTIAL, orPROVENANCE); - resolution state (
RESOLVED,UNRESOLVED,AMBIGUOUS,MISSING_TARGET, orSYMBOLIC_BOUNDARY); - source locations where available and permitted;
- whether impact traversal should propagate through the relation.
Static analysis reports potential dependencies. A parsed branch in a Script or DMN expression need not execute in every runtime invocation.
Artifact and node types
The following groups describe the canonical node types returned by graph and export APIs. Not every installation or provider produces every type.
Ownership and API surface
| Type | Meaning |
|---|---|
MICROSERVICE | A deployable tSM service that owns entities, configuration, or API operations. |
MODULE | A functional/configuration module and its association with a microservice or configuration type. |
PUBLIC_API_OPERATION | One public REST operation, including HTTP method, path, operation ID, and handler methods. |
API_CLIENT | A typed client exposed in the runtime SpEL context, including its bean path, API version, and entity type when known. |
API_CLIENT_OPERATION | One autocomplete-visible client method that a Script can invoke. It is separate from the server REST operation that it calls. |
MESSAGE_CHANNEL | A message topic, queue, or other integration channel declared by a provider. |
ENTITY_CATALOG | A catalog grouping of entity definitions exposed by one provider. |
CONFIGURATION_ITEM | A generic configuration artifact when a more specific canonical kind is not available. |
Entity schema
| Type | Meaning |
|---|---|
ENTITY_TYPE | A canonical business entity such as Customer, discovered primarily from service code. |
ENTITY_SUBTYPE | A configured variant of an entity, for example Customer/B2B. |
ENTITY_SPECIFICATION | The configuration that selects Characteristics and behavior for an entity subtype. |
ENTITY_SPEC_CHARACTERISTIC | The assignment of one Characteristic to one Entity Specification. |
ENTITY_ATTRIBUTE | A flattened addressable attribute with type, requiredness, origin, and schema pointer. |
CHARACTERISTIC | A reusable configured schema fragment added to an entity or Form. |
DATA_SCHEMA | A JSON Schema document associated with an entity, Form, or Characteristic. |
REFERENCE_DEFINITION | A declared field-level reference to another entity or configuration artifact. |
CONFIG_TYPE | A configuration category used to classify Forms, Characteristics, Listings, and related artifacts. |
Storage and search representations
| Type | Meaning |
|---|---|
JPA_ENTITY | A concrete JPA entity or mapped superclass representing a logical entity. Its facets include the Java class, entity/table name, schema, and abstract flag. |
JPA_ATTRIBUTE | A persisted field, including inherited and embedded paths, column/join metadata, identifier status, and JPA association cardinality. |
ELASTIC_DOCUMENT | A Spring Data Elasticsearch document representing a logical entity, including its class and index name. |
ELASTIC_FIELD | An Elasticsearch field, including its serialized path, field type, normalizer/analyzer, and nested/object structure. |
These nodes do not replace the public entity schema. A logical ENTITY_ATTRIBUTE is the join point for the public DTO,
JPA, and Elasticsearch representations. Its representations facet states where the path exists. A database-only
field can therefore be addressed by a Listing without being falsely advertised as a public DTO property. The
tsm:mapsToEntityAttribute relation joins each physical field to that logical path;
tsm:extendsRepresentationType retains mapped-superclass inheritance and
tsm:referencesRepresentationType retains physical associations.
Annotated JPA or Elasticsearch classes that do not correspond to a registered Public DTO are still published as implementation-only entity types. They have physical representation nodes and logical join-point attributes, but no invented Public schema. This keeps private join tables, searchable documents, and association targets available to diagnostics and navigation.
Specifications, subtypes, and characteristic attributes
An ENTITY_SUBTYPE selects an ENTITY_SPECIFICATION; the subtype does not own a separate copy of the characteristic
schema. Several subtype rows may select the same specification. The specification owns its
ENTITY_SPEC_CHARACTERISTIC assignments and those assignments point to reusable CHARACTERISTIC definitions.
For each entity type, Config & Forms publishes one canonical chars.* attribute catalog built from all current
specifications. The catalog is a union of attributes that may occur on the entity. Each attribute records the
specification IDs/codes in which it occurs and presentInAllSpecifications, so a consumer can also obtain the strict
intersection. A generic Listing field such as chars.capacity targets this one entity-level attribute, avoiding the
ambiguity that would result from publishing one identically aliased attribute node per specification.
Forms and presentation configuration
| Type | Meaning |
|---|---|
FORM | A Form definition, its data schema, UI expressions, actions, and declared dependencies. |
FORM_ELEMENT | A significant element inside a Form when separate navigation or usage evidence is useful. |
LISTING | A configured list/table view. |
LISTING_COLUMN | A column and its value or rendering expression. |
LISTING_PROFILE | A profile that customizes a Listing for a context or user group. |
DASHBOARD | A configured dashboard and its referenced resources. |
MENU_ITEM | A navigation item and its target. |
The backend does not attempt to inventory every available frontend widget. It maintains descriptors only for widgets
whose configuration contains a statically extractable Data Model dependency, such as a target Register, Form, Script,
Characteristic, or entity type. An unknown or presentation-only widget is silently ignored and does not make the Form
PARTIAL; unknown actions and data sources remain diagnostics because those constructs are executable dependency
boundaries.
Executable configuration
| Type | Meaning |
|---|---|
SCRIPT | A stored Script plus structured metadata and statically discovered dependencies. |
EXPRESSION | A separately addressable configured expression, for example a mnemonic or Listing expression. Embedded Form JEXL is represented by direct semantic edges from its owner and does not create one node per expression. |
SCRIPT_BINDING | A named binding through which Scripts are exposed to expressions. |
EVENT_BINDING | A configured event-to-Script or event-to-action association. |
MNEMONIC_CONFIG | A configured mnemonic expression used to construct identifiers. |
Registers
| Type | Meaning |
|---|---|
REGISTER | The definition of a code list/register, including schema and dependencies. |
REGISTER_VALUE | A register value only when it carries model-relevant structure or references. Ordinary large code-list values are intentionally not expanded into graph nodes. |
BPMN and DMN
| Type | Meaning |
|---|---|
PROCESS_DEFINITION | The logical BPMN process definition. |
PROCESS_VERSION | One selected deployed/versioned BPMN model. Providers index the latest deployed and highest semantic version instead of loading every large historical XML document. |
BPMN_ELEMENT | A relevant task, event, gateway, or other BPMN element and its dependencies. |
TASK_TEMPLATE | A reusable process task configuration/template. |
DMN_DEFINITION | The logical DMN definition. |
DMN_VERSION | One selected deployed/versioned DMN model. |
DMN_ELEMENT | A decision, input, output, or another relevant element inside a DMN model. |
Source types and providers
A provider is a microservice that owns authoritative artifacts. A source is the smallest independently replaceable publication unit. For example, each Form and Script is a separate source; one invalid Script does not replace or remove the successfully published graph of another Script.
Config & Forms publishes FORM, CHARACTERISTIC, ENTITY_SPECIFICATION, SCRIPT, REGISTER, LISTING,
MNEMONIC_CONFIG, CONFIG_TYPE, ENTITY_TYPE_METADATA, MICROSERVICE, MODULE, and
API_CLIENT_CATALOG sources. Business microservices publish native entity/API sources. Process-aware services
publish selected BPMN and DMN definition versions. Source kinds are extensible strings, while graph node kinds form the
shared canonical vocabulary described above.
Scripts, typed clients, and server APIs
The model does not treat a Java/SpEL client method as if it were a REST endpoint. For example, the expression
@order.order.get('SO-1') is represented as a complete invocation chain:
SCRIPT --INVOKES_API_CLIENT_OPERATION--> API_CLIENT_OPERATION
API_CLIENT --EXPOSES_API_CLIENT_OPERATION--> API_CLIENT_OPERATION
API_CLIENT_OPERATION --CALLS_API_OPERATION--> PUBLIC_API_OPERATION
The client and its operation also link to ENTITY_TYPE when that information is available. This lets a consumer answer
different questions without conflating layers: which Scripts use a client, which client method performs the call, which
HTTP endpoint receives it, and which entity is read or changed.
No Data Model-specific client annotation is required. Config & Forms discovers the same classes and methods already
published to the SpEL console through @SpelAutocomplete. The existing name placeholder supplies the entity type, a
vN package segment supplies the API version, and conventional method names supply coarse CRUD capabilities. Standard
v2 client operations connect to server operations by the exact (entityType, methodName) pair. Explicit OpenAPI
operation IDs take precedence when a client declares one. The coarser (entityType, capability) identity remains
available for navigation and grouping, but is not the primary match because several endpoints may share a capability.
Manual EntityType configuration is supplemental. The system resolves structure and relationships from code wherever
possible, because manually repeated facts can become inconsistent. Human-friendly descriptions, classifications, and
other facts that cannot be inferred safely remain appropriate in EntityType.
Reading schemas and the catalog
All public endpoints are rooted at /api/v2/data-model on Config & Forms. Through a gateway, prepend the Config & Forms
service route used by the installation.
| Purpose | Endpoint |
|---|---|
| Current online manifest and revision | GET /api/v2/data-model/manifest |
| Search entities and configuration artifacts | GET /api/v2/data-model/index |
| Form schema by UUID or code | GET /api/v2/data-model/forms/{idOrCode} |
| Characteristic schema by UUID or code | GET /api/v2/data-model/characteristics/{idOrCode} |
| All known variants of an entity | GET /api/v2/data-model/entities/{entityType} |
| Lightweight entity variant list | GET /api/v2/data-model/entities/{entityType}/summary |
| One exact stable variant | GET /api/v2/data-model/entities/{entityType}/variants/{variantId} |
| One exact entity variant | GET /api/v2/data-model/entities/{entityType}/subtypes/{subtypeCode} |
| Complete current gzip export | GET /api/v2/data-model/export |
| Selected JSON export sections | GET /api/v2/data-model/export/sections |
Entity example
GET /api/v2/data-model/entities/Customer/subtypes/B2B
Accept: application/json
A shortened response illustrates the important fields:
{
"entityType": "Customer",
"subtypeCode": "B2B",
"dataSchema": {
"schema": {
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"segment": { "type": "string" }
}
}
},
"attributes": [
{ "path": "id", "type": "string", "required": true, "source": "NATIVE" },
{ "path": "segment", "type": "string", "required": false, "source": "CHARACTERISTIC" }
],
"references": [],
"problems": [],
"meta": { "status": "CURRENT" }
}
Use an exact subtype endpoint when the caller knows the subtype. The entity-only endpoint deliberately returns all variants; silently choosing one would hide ambiguous installation configuration.
Online explorers should load /manifest, then the entity /summary, and request only the selected opaque variantId.
The manifest returns a numeric modelRevision. A client that wants to detect a revision boundary may send it in
X-TSM-Data-Model-Revision on subsequent schema and graph reads. HTTP 412 DataModel.RevisionChanged means that newer
content was committed; restart the multi-request operation from a new manifest. The API deliberately does not serve
historical revisions and does not use ETags.
A single online request always reads committed generations only. Several unpinned requests can legitimately observe a new revision between calls—for example, search may return the previous Form while its later detail call returns the replacement. Pinning detects this boundary; it does not retain the older revision.
Full export
GET /api/v2/data-model/export/sections?include=ENTITIES&include=NODES&include=EDGES
Accept: application/json
For a locally running Config & Forms service, the complete URL is:
http://localhost:8080/tsm-config-form/api/v2/data-model/export/sections?include=ENTITIES&include=NODES&include=EDGES
When calling through a gateway, use the gateway origin with the same /tsm-config-form/api/v2/data-model/... route and
send the installation's API key using its standard authentication header. Do not store an API key in source code or
documentation.
The selected JSON export is useful for diagnostics and focused offline work. A production UI should use the manifest, paged index, entity summary/variant, and bounded graph endpoints for normal interaction.
The complete export can be hundreds of megabytes and is therefore generated directly as a temporary gzip file. The service does not retain an uncompressed response or a reusable gzip byte array in heap:
curl --fail --location \
-H 'Accept: application/gzip' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
'http://localhost:8080/tsm-config-form/api/v2/data-model/export' \
--output data-model-export.json.gz
gzip --test data-model-export.json.gz
The media type is application/gzip; the file content itself is gzip, so the response does not use
Content-Encoding: gzip. Global metadata occurs once at the export root rather than being repeated in every Form and
Characteristics record. Only one complete export is assembled concurrently per Config & Forms instance.
Export requires at least one successfully published source. On a new installation it therefore returns
503 DataModel.Unavailable while the first full refresh is still building the model. Later refreshes keep the
last-known-good source state readable until replacement data is committed. Complete read-model operations scan source
metadata in stable pages and support up to 100,000 sources by default; the smaller administrative status response remains
intentionally bounded as described below.
The exporter uses deterministic Elasticsearch search_after pages. It records the committed modelRevision before
assembly and verifies it again before and after writing the gzip file. If publication crosses the export window, the
service discards the temporary file and retries the complete assembly up to a bounded limit. Repeated concurrent
changes return 503 DataModel.Unavailable instead of a mixed file. This gives a consistent export without retaining
historical model copies.
Exploring dependencies and impact
Resolve a business identifier
Graph operations use stable node IDs. Resolve a human-facing alias first:
GET /api/v2/data-model/graph/resolve?namespace=script:code&key=Customer.RecalculateRisk
The response distinguishes an exact match from no match and ambiguity. Batch resolution is available at
POST /api/v2/data-model/graph/resolve-batch.
Read a node and its direct edges
GET /api/v2/data-model/graph/nodes/{nodeId}
GET /api/v2/data-model/graph/nodes/{nodeId}/edges?direction=INCOMING&page=0&size=50
OUTGOINGanswers “what does this node depend on?”INCOMINGanswers “what directly uses this node?”BOTHis useful for a diagram centered on one artifact.
Nodes can also be searched with the paged GET /api/v2/data-model/graph/nodes endpoint.
Traverse possible impact
POST /api/v2/data-model/graph/traverse
Content-Type: application/json
{
"startNodeIds": ["{nodeId}"],
"direction": "INCOMING",
"mode": "IMPACT",
"maxDepth": 3,
"minimumConfidence": "MEDIUM",
"includeContextual": false,
"maxNodes": 500,
"maxEdges": 1000,
"timeoutMillis": 2000
}
Traversal is intentionally bounded. Inspect truncated and truncationReason; a truncated result is not a complete
impact set. Use POST /graph/paths for bounded paths between known nodes and POST /graph/usage-summaries when a UI
needs counts rather than all edges.
Understand SpEL dependencies
SpEL analysis publishes domain meaning rather than every parser token. Typed API calls, stored-script calls, register reads, and known entity-property accesses become normal graph relations to canonical nodes. Local variables, arbitrary helper methods, and untyped properties remain bounded analyzer diagnostics and are not exported as synthetic nodes.
For example, if autocomplete metadata identifies @crm.address.get(...) as returning CrmAddress, this script:
#address = @crm.address.get(#addressId);
#address.lon
creates an API-client-operation dependency and a READS_ATTRIBUTE relation to CrmAddress.lon. It does not create
nodes such as method:get/1, variable:address, or property:#address.lon. Search and bulk results are not type-inferred
because their element flow is not unambiguous.
Understand JEXL dependencies
JEXL used in Form configuration is parsed by a bounded structural analyzer; it is not JavaScript and the backend does
not claim complete language or runtime data-flow coverage. Supported $context, entity, Script, Register, and typed
API-client patterns produce direct semantic relations from the owning FORM or FORM_ELEMENT. For example, an Account
form expression that reads $context.entity.customer.id records a READS_ATTRIBUTE dependency with its JSON pointer,
role, variable, root entity type, and path as edge evidence. Resolution may target the exact reachable attribute or a
safe symbolic boundary when the complete chain cannot be proven.
The raw expression text is not exported as a synthetic node merely because it was found in Form JSON. Safety ceilings
are reported through inventory facets and diagnostics such as JEXL_SCAN_LIMIT_REACHED; a count at the configured
limit must therefore not be presented as a proven total.
Read diagnostics
GET /api/v2/data-model/graph/diagnostics?providerId=tsm-customer&code=TARGET_NOT_FOUND&page=0&size=100
Common diagnostics include:
| Code/category | Interpretation |
|---|---|
TARGET_NOT_FOUND | A symbolic reference currently has no unique canonical target. It may be missing configuration, a dynamic/external boundary, or a target not yet indexed when the source was processed. |
SPEL_PARSE_ERROR | Structural metadata is retained, but dependencies could not be extracted from one malformed expression. |
*_ANALYSIS_LIMIT_EXCEEDED | A safety bound prevented complete static analysis; the source remains visible and partial. |
FORM_ATTRIBUTE_PATH_UNSUPPORTED | A legacy Form property remains in JSON Schema but cannot be represented as a canonical dot-separated attribute path. |
SOURCE_ANALYSIS_FAILED | The provider could not safely produce a contribution for the source. If available, the previous successful generation remains readable. |
Warnings indicate known incompleteness and do not remove otherwise valid structural data. Errors make the affected
source FAILED and preserve its last-known-good generation where one exists.
Using the model from SpEL
The global @dataModel object exposes the same current model. Its methods carry console-visible annotation
documentation and autocomplete metadata.
// All variants of an entity
@dataModel.entity.get("Customer")
// One exact variant
@dataModel.entity.get("Customer", "B2B")
// Direct users of a Script
@dataModel.usage.direct("SCRIPT", "Customer.RecalculateRisk")
// Bounded incoming impact of a Form
@dataModel.impact.of(
"FORM",
"Customer.B2B.Detail",
{"maxDepth": 3, "minimumConfidence": "MEDIUM"}
)
// Lightweight status and full administrative status
@dataModel.status.get()
@dataModel.status.summary()
@dataModel.status.details()
Additional areas include form, characteristic, index, export, graph, and diagnostics. Schema get methods
return null for an unknown object. Usage and impact methods require an exact node resolution and report not-found or
ambiguous identifiers instead of returning a misleading empty result.
Refresh lifecycle
Each owning service publishes deterministic, paged contributions. Config & Forms reads them, analyzes semantic
references, and atomically replaces the current generation per source. A change signal normally schedules a scoped
CONDITIONAL refresh. Periodic reconciliation recovers missed signals and retries sources that could not be refreshed.
A refresh is asynchronous:
- submit a scope and mode;
- receive HTTP
202and arunId; - poll the run until
CURRENT,PARTIAL, orFAILED; - inspect administrative status and wait for any follow-up reconciliation run;
- evaluate source states and diagnostics, not only the parent run state.
Administrative operations require Config.DataModel.Administer.
POST /api/v2/data-model/admin/refresh
GET /api/v2/data-model/admin/runs/{runId}
GET /api/v2/data-model/admin/runs/{runId}/summary
GET /api/v2/data-model/admin/runs/{runId}/problems?page=0&size=50
GET /api/v2/data-model/admin/status
GET /api/v2/data-model/admin/status/summary
Use the summary endpoints for polling. They omit source payloads and individual problems, expose aggregate provider/run
counters, and report the exact selected-source total when the run has an explicit source-key scope. Fetch problem pages
only when a run is partial or failed. The retained problem buffer prioritizes errors over warnings and truncated=true
means that additional diagnostics exceeded its configured retention bound.
The detailed administrative source list is a bounded operational view. On a large installation it returns the first configured
number of source statuses and adds SOURCE_STATUS_TRUNCATED with the exact total. This does not truncate refresh
processing: the coordinator scans the complete source catalog internally. Use filtered diagnostics, graph paging, or a
full export instead of treating the administrative list as a bulk-data endpoint.
Normal refresh
POST /api/v2/data-model/admin/refresh
Content-Type: application/json
{
"mode": "CONDITIONAL"
}
Scoped refresh
Select exact source keys when they are known:
{
"providerId": "tsm-customer",
"sourceKeys": ["native-entity:Customer"],
"mode": "FORCE"
}
Alternatively, select a semantic entity or configuration scope:
{
"providerId": "tsm-customer",
"entityTypes": ["Customer"],
"mode": "FORCE"
}
sourceKeys cannot be combined with entityTypes or configTypes in one request. Empty scope collections do not
restrict the run. A request can select a provider, up to 1,000 source keys, up to 100 entity types, or up to 500
configuration types.
Refresh modes
| Mode | Use |
|---|---|
CONDITIONAL | Normal and cheapest refresh. Unchanged sources use their revision/ETag and are not analyzed again. |
FORCE | Fetch and analyze every source in the selected scope even if its revision is unchanged. |
REBUILD | Reconstruct the current read model from authoritative contributions without first deleting the readable graph. Use after model implementation or persistence changes. |
RESOLVE_ONLY | Re-resolve symbolic targets from stored contributions without calling providers. Use after missing targets have been added; it does not re-analyze changed source content. |
Equivalent SpEL administration calls are:
@dataModel.status.refresh()
@dataModel.status.refresh({
"providerId": "tsm-customer",
"mode": "FORCE"
})
@dataModel.status.run("dm-refresh-...")
@dataModel.status.runSummary("dm-refresh-...")
@dataModel.status.problems("dm-refresh-...", 0, 50)
@dataModel.status.summary()
@dataModel.status.details()
Why data can remain stale or partial
The model is eventually consistent and is published source by source. During a large refresh, an early source may refer
to a target that is published later. After the parent refresh, reconciliation re-resolves affected stored contributions.
For that reason, do not declare a refresh complete while admin/status.activeRuns is non-empty.
Source and aggregate states mean:
| State | Meaning |
|---|---|
UPDATING / INDEXING | One or more source generations are being processed. |
CURRENT | The selected materialized data is current and has no known limiting diagnostic. |
PARTIAL | Useful data was published, but some references or analyzers are incomplete. |
STALE | A readable last-known-good result exists but is older than its authoritative source or freshness policy. |
FAILED | The latest source attempt failed; a previous successful generation may remain readable. |
DELETED | The authoritative source was removed and its materialized generation has been retired. |
Typical causes are an unavailable provider, malformed legacy Form schema, invalid SpEL/BPMN/DMN, a timeout, a missed
change event, or a reference to a target that does not exist. A TARGET_NOT_FOUND warning is not automatically a broken
source: inspect its namespace, location, evidence, and provider coverage.
Recommended recovery sequence:
- run global
CONDITIONALafter normal bulk configuration changes; - use scoped
FORCEif unchanged revision metadata prevented required re-analysis; - use
RESOLVE_ONLYafter adding targets when the stored sources themselves did not change; - use
REBUILDafter analyzer/model upgrades or when validating the complete installation; - poll the accepted run and all follow-up active runs, then review failed/stale sources and diagnostics.
Security
Normal reads require Config.DataModel.View. Exact evidence and source locations require
Config.DataModel.Evidence. Refresh and administrative status require Config.DataModel.Administer. An API key or user
must carry the corresponding permissions; having general access to Config & Forms is not sufficient.
Architecture summary
Authoritative data stays in Config & Forms and the individual business/process services. The provider starter exposes capabilities and deterministic paged source contributions from every participating service. Providers derive native entity structure and relationships from code and add only appropriate descriptive configuration metadata.
Config & Forms acts as coordinator and materializer. It fetches contributions, performs bounded static analysis, resolves canonical aliases, and stores current nodes, edges, sources, and diagnostics in Elasticsearch. Refresh runs and coordination use a separate operational store. Per-source atomic replacement and last-known-good preservation keep the read model available when one artifact is invalid.
Config & Forms also inventories its real autocomplete runtime to publish typed API clients and their operations. Business services publish the corresponding server-side Spring MVC operations. Canonical aliases join these independent sources during resolution, so Config & Forms does not need to load or invoke the clients while analyzing a Script.
Public API and @dataModel reads never fan out across all microservices and never parse source documents on demand.
They read the already materialized committed revision. Inside Config & Forms the typed client uses a local transport; other
services use the same client contract through the standard tSM service-client infrastructure.
Publication, concurrency, and caches
Each source document keeps two generation numbers. attemptGeneration fences the current refresh attempt;
publishGeneration remains the last successfully committed generation. Nodes, edges, and diagnostics are staged under
the attempt token, validated, and made visible by one compare-and-set update of the small model-head document. Failed or
superseded attempts never enter normal reads. Two parallel refreshes may both perform analysis, but only the attempt that
still owns the source lease can commit. A bounded refresh page combines its successfully finalized source pointers into
one head compare-and-set update, which increments the global modelRevision once. A targeted single-source refresh is
one such batch. This preserves the visibility boundary without adding two remote head operations for every source.
Normal Elasticsearch queries include a terms lookup against the head's committed generation tokens. Old documents may therefore remain briefly after commit without being visible. Their deletion is maintenance, not part of correctness; failure to delete them does not misreport an already committed publication as failed.
The read path intentionally has only two local caches:
- Model head cache: one small projection of the head document, cached for at most five seconds. Completion of a refresh run emits a Kafka head event, and every Config & Forms replica has its own consumer group so all replicas invalidate immediately. During a long multi-source run, and after a delayed or missed event, the TTL is the safety bound. Internal generation tokens remain in Elasticsearch and are excluded from the manifest read.
- Committed source-state cache: a lazy access-ordered cache of at most 4,096 full source documents. Schema requests load only sources reached from the requested Form, Characteristic, or entity specification chain. A revision change or Kafka event clears it.
There is no global in-memory schema/graph cache, response cache, gzip cache, or ETag cache. This keeps HA replicas independent and bounded: either replica can answer from the same Elasticsearch head, while Kafka only shortens the maximum stale-head observation window.