Service Architecture Migration
Table of Contents
Overview
This document describes the plan to migrate ORE Studio's service architecture to a clean three-layer model with clear separation of concerns and testability at every layer.
Current state
Services are structured as two components:
ores.iam ← domain + repository + service + messaging (mixed) ores.iam.service ← executable: wires everything together
Clients (ores.qt, ores.shell, ores.cli) link against ores.iam
to access protocol types, dragging in database, repository, and service
logic they never use. Protocol types have no dedicated tests.
The ores.http.server binary mixes server infrastructure with
route handlers for IAM, refdata, assets, variability — tightly coupling
the HTTP server to every service domain.
Target architecture
Each service domain becomes three distinct components:
ores.iam.api ← shared contract: domain types (POCOs + IO) and
messaging protocol (request/response structs,
nats_subject constants); no DB, no handlers
ores.iam.core ← implementation: handlers, service logic, repository,
DB access; depends on ores.iam.api
ores.iam.service ← executable; links both, wires NATS and DB
The separation is driven by a key invariant: ores.iam.api contains
everything that appears on the wire — domain types that are serialised
as request/response fields belong here, not just protocol envelopes.
Repository and handler code depends on domain types but is private
implementation detail; it lives in ores.iam.core.
Clients link only ores.iam.api. The dependency graph is strict and
expresses intent:
ores.qt ──→ ores.iam.api ores.qt ──→ ores.compute.api ... ores.iam.core ──→ ores.iam.api (implements the protocol) ores.iam.service ──→ ores.iam.core (runs the service)
The ores.http.server binary becomes a thin orchestrator:
ores.http.routes ← all HTTP route registrations (new component)
ores.http.server ← server infrastructure only: main, application,
config, parser, NATS setup; links ores.http.routes
Scope
Ten service libraries require splitting:
| Library | Protocol headers | Route file in http.server? |
|---|---|---|
| ores.iam | 23 | yes (iam_routes) |
| ores.refdata | 47 | yes (risk_routes) |
| ores.compute | 19 | no |
| ores.dq | 22 | no |
| ores.assets | 3 | yes (assets_routes) |
| ores.variability | 3 | yes (variability_routes) |
| ores.scheduler | 3 | no |
| ores.reporting | 9 | no |
| ores.trading | 3 | no |
| ores.synthetic | 3 | no |
ores.http already has messaging/http_info_protocol.hpp (created in
the service discovery feature) and is a working example of the target
*.api pattern.
What moves where
Types components (ores.{service}.api)
Move from ores.{service}/include/ores.{service}/:
- All
domain/*.hppPOCOs → new component'sinclude/ores.{service}.api/domain/ domain/*_io.hpp(JSON + table serialisation helpers) → new componentmessaging/*_protocol.hppfiles → new component'sinclude/ores.{service}.api/messaging/nats_subjectconstants → new component
Stay in ores.{service}.core:
messaging/*_handler.hpp/.cppfiles (implement the protocol using repo/service)messaging/registrar.hpp/.cpp(registers handlers; called by the service executable)repository/(database access, SQL mappers)service/(business logic)
The new types component has:
include/ores.{service}.api/— domain types + protocol headerstests/— unit tests for serialisation round-tripsCMakeLists.txt— INTERFACE target if header-only (no.cppfiles for IO helpers that are pure templates); static lib otherwise
HTTP routes (ores.http.routes)
Move from ores.http.server/src/routes/ and
ores.http.server/include/ores.http.server/routes/:
All *_routes.{hpp,cpp} files → new ores.http.routes component.
ores.http.routes depends on: ores.http.lib, ores.iam.core,
ores.refdata.core, ores.assets.core, ores.variability.core
(the full core libs — handlers are implemented there).
ores.http.server drops the route dependencies and links
ores.http.routes instead. The server itself retains only:
src/app/application.{hpp,cpp}— server setup, NATS initsrc/app/host.{hpp,cpp}src/config/— options, parsersrc/messaging/— HTTP info handler and registrarsrc/main.cpp
Migration phases
Phase 1 — Split ores.iam and ores.compute (highest value)
These are the largest and most-used components.
Steps:
- Create
projects/ores.iam.api/scaffold via codegen (component profile then hand-adjust: no domain stub needed). - Move all domain POCOs and IO helpers from
ores.iamintoprojects/ores.iam.api/include/ores.iam.api/domain/. - Move all
ores.iam/include/ores.iam/messaging/*_protocol.hppintoprojects/ores.iam.api/include/ores.iam.api/messaging/. - Rename
ores.iamproject toores.iam.core; update#include "ores.iam/..."→#include "ores.iam.core/..."throughout, and update all dependent CMakeLists. - Update
ores.iam.coreto#include "ores.iam.api/..."and addores.iam.apias a PUBLIC dependency. - Update clients (
ores.qt,ores.shell,ores.cli) to linkores.iam.apiinstead ofores.iam(nowores.iam.core). - Write serialisation round-trip tests in
ores.iam.api/tests/: for each protocol type, serialize and deserialise viarfl::json::write/rfl::json::readand assert field equality. - Repeat for
ores.compute.api.
PR title: [arch] Extract ores.iam.api and ores.compute.api
Phase 2 — Remaining service splits
Repeat Phase 1 steps for:
ores.refdata.api/ores.refdata.core(47 headers — largest)ores.dq.api/ores.dq.coreores.reporting.api/ores.reporting.coreores.scheduler.api/ores.scheduler.coreores.assets.api/ores.assets.coreores.variability.api/ores.variability.coreores.trading.api/ores.trading.coreores.synthetic.api/ores.synthetic.core
Do in batches of 2-3 to keep PRs reviewable.
PR title per batch: [arch] Extract {service} types and core components
Phase 3 — ores.http.routes extraction
Steps:
- Create
projects/ores.http.routes/component scaffold. - Move all
*_routes.{hpp,cpp}fromores.http.serverinto the new component under the same internal structure. ores.http.routes/CMakeLists.txtlinks PUBLIC toores.http.liband PRIVATE to the core libs it delegates to (ores.iam.core,ores.refdata.core, etc.).ores.http.server/CMakeLists.txtdrops those core lib deps and linksores.http.routesinstead.- Build and verify end-to-end.
PR title: [arch] Extract HTTP routes into ores.http.routes
Phase 4 — ores.http.server cleanup
After Phase 3, ores.http.server links only:
ores.http.lib ores.http.routes ores.nats.lib ores.service.lib ores.telemetry.lib ores.utility.lib ores.database.lib (for DB setup in application.cpp)
At this point ores.http.server is a pure infrastructure component:
server lifecycle, config parsing, NATS service discovery, signal
handling. No domain knowledge.
PR title: [arch] Clean up ores.http.server dependencies
Naming conventions
| Pattern | Contains |
|---|---|
ores.{service}.api |
domain POCOs, IO helpers, *_protocol.hpp, nats_subject constants |
ores.{service}.core |
handlers, service logic, repository, DB access |
ores.{service}.service |
executable: main, app, config |
ores.http.routes |
HTTP route handlers for all services |
ores.http.server |
HTTP server infrastructure only |
Key invariant
Clients (
ores.qt,ores.shell,ores.cli) MUST NOT link any*.coreor service implementation library. They link only*.apicomponents and infrastructure libs (ores.nats,ores.eventing, etc.).
This is enforced structurally: the *.api component has no database,
no repository, no service dependencies — so linking it is safe for any
client. The *.core component is implementation detail shared only
between the service executable and ores.http.routes.
Testing approach
Each ores.{service}.api component includes its own Catch2 test
binary. Tests cover:
- Round-trip serialisation:
rfl::json::writethenrfl::json::readproduces equal struct. - Subject names: assert
nats_subjectconstants match expected strings (guards against accidental renames breaking the wire protocol). - Required fields: confirm that missing required fields produce a deserialisation error rather than silent defaults.
No database, no NATS connection required — tests run entirely in-process.
Open questions
- Should
ores.{service}.apibe a shared library (.lib) or a CMake INTERFACE target? Answer: use INTERFACE if no.cppfiles are needed (pure header templates); add a.libtarget if IO helpers have non-trivial implementation. - HTTP routes for
ores.compute(file upload/download) are infrastructure-only and do not use service domain types. They can stay inores.http.routesor remain inores.http.serverdepending on team preference. - The
ores.cliclient also uses repositories directly for bulk operations (import/export). In Phase 1,ores.clicontinues to linkores.iam.corefor these operations; the repository is not separately exposed.