Architecture
Model Forge is an embedded Java/Spring Boot runtime for data modelling, versioning and integration configuration within the CIVITAS/CORE platform.
It is not a standalone REST service. Host applications integrate Model Forge as Maven dependencies and call the public Java facade.
Core Goals
- JSON Schema 2020-12 as canonical data modelling format.
- PostgreSQL-backed artifact registry as source of truth.
- Global, stable artifact identities based on CORE URNs.
- Traceable schema and artifact dependencies.
- CORE-IR artifacts and DataSet manifests for downstream pipeline engines.
- A small, transport-neutral Java integration boundary.
The decisions behind these goals are documented as Architecture Decision Records.
Module Model
host application
-> model-forge-spring-boot-starter (auto-configuration)
-> model-forge-contract (ModelForge facade + records)
-> model-forge-runtime (facade impl -> use cases -> ports -> adapters)
| Module | Responsibility |
|---|---|
model-forge-contract | Public records (commands, results, queries), the ModelForge facade interface and URN helpers. No Spring, web, JDBC, Flyway, PostgreSQL, Servlet, OpenAPI or HAL dependencies. |
model-forge-runtime | The embedded runtime: facade implementation, ports, use cases (import, validation, views, graph), the JDBC/JdbcClient PostgreSQL adapter with Flyway migrations, and the HTTP integration adapters. Internal slice boundaries (core/application/persistence/integrations packages) are enforced by ArchUnit tests. |
model-forge-spring-boot-starter | Spring Boot auto-configuration that wires the facade and adapters into a host application. |
model-forge-admin-ui | Local Wicket developer/debug UI. A consumer of the library through the public facade, not part of the library boundary. |
Public Boundary
Host code should depend on the public facade, for example ModelForge, and on
contract records. It should not call application services, persistence classes
or internal utilities directly.
import de.civitascore.modelforge.facade.ModelForge;
import org.springframework.stereotype.Service;
@Service
class ImportWorkflow {
private final ModelForge modelForge;
ImportWorkflow(ModelForge modelForge) {
this.modelForge = modelForge;
}
}
The host application owns external API design, authentication, authorization, request limits and operational exposure. Model Forge provides embedded domain capabilities. See portal-backend Integration & Division of Labor for how this splits data ownership once a host integrates Model Forge.
Ports And Adapters
Application code depends on ports, not on infrastructure:
| Port | Productive adapter |
|---|---|
ArtifactRegistry | PostgresArtifactRegistryClient in model-forge-runtime |
RemoteSchemaRepository | RemoteSchemaFetcher in model-forge-runtime (SSRF-guarded HTTP schema fetching) |
XRepositoryCatalog | XRepositoryClient in model-forge-runtime |
XsdSchemaConverter | XSD to JSON Schema converter |
This keeps import, validation, graph and view logic testable without web or database concerns.
PostgreSQL Registry
The host provides spring.datasource. Model Forge reuses that datasource and
runs its own Flyway migration in a dedicated schema:
model-forge:
registry:
schema: model_forge
migration-table: model_forge_schema_history
Model Forge does not register JPA entities. The registry stores dynamic JSON-/XSD-payloads, version metadata and artifact references using explicit SQL.
Stored Artifacts
DataSet Manifest
{
"$schema": "https://civitasconnect.digital/core-dataset/v1",
"id": "urn:core:platform:civitas:dataset:common:SensorData:nxsphawneu:1.0.0",
"title": "Sensordaten-Import",
"version": "1.0.0",
"datastructureRefs": ["urn:core:...:datastructure:...:SensorData:1.0.0"],
"mappingRefs": ["urn:core:...:mapping:...:sensor-to-obs:1.0.0"],
"pipelineRefs": ["urn:core:...:pipeline:...:MQTT-to-SQL:1.0.0"],
"dataSourceRefs": ["urn:core:...:datasource:...:mqtt-sensor:1.0.0"],
"dataSinkRefs": ["urn:core:...:datasink:...:sql-obs:1.0.0"]
}
Element
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:core:platform:civitas:element:common:SensorReading:d28s38wfmi:1.0.0",
"title": "SensorReading",
"type": "object",
"required": ["sensorId", "temperature", "observedAt"],
"properties": {
"sensorId": { "type": "string" },
"temperature": { "type": "number" },
"observedAt": { "type": "string", "format": "date-time" },
"location": { "$ref": "urn:core:platform:civitas:element:common:GeoPoint:3ak90vqrog:1.0.0" }
}
}
Every $ref to a CORE URN becomes a stored artifact reference and a graph edge.
The full format of Mappings, Pipelines, DataSources and DataSinks is specified
in the CORE-IR Reference.
Runtime State
PostgreSQL is the source of truth. The only in-memory state is the dependency graph, derived and rebuilt from the registry at startup.
For the first embedded version the documented operating model is single-writer. If a host later runs multiple write-capable instances, concurrency and cache invalidation need a separate architecture decision.
Enforced Boundaries
ArchUnit tests guard the important boundaries:
- Contract is transport- and infrastructure-free.
- Application code does not depend on Spring Web, Servlet, HAL or OpenAPI.
- Application code depends on registry ports, not PostgreSQL adapters.
- The Spring Boot starter registers no controllers or web-security chains.
Technology Decisions
| Decision | Chosen technology | Rationale |
|---|---|---|
| Schema format | JSON Schema 2020-12 | Open standard, JSON-native, broad tooling |
| Artifact registry | PostgreSQL + Flyway | Full reference graph, JSONB payloads, SemVer metadata, no additional service |
| Persistence access | JDBC/JdbcClient | Explicit fit for dynamic artifact payloads and version tables |
| Validation | networknt json-schema-validator | JSON Schema 2020-12 support and custom annotation keywords |
| Host integration | Spring Boot 4 starter | Matches portal-backend and reuses host datasource/transactions |