Skip to content

Latest commit

 

History

History
721 lines (538 loc) · 57.4 KB

mORMot2-SAD-Recommended-Patterns.md

File metadata and controls

721 lines (538 loc) · 57.4 KB

Recommended Patterns & Default Toolbox

A Possible Pattern for Using mORMot 2

mORMot 2 is a large framework with many ways to do the same thing. That flexibility is a strength, but newcomers consistently ask the same question: "What is the basic set of types, classes and configuration I should reach for by default?" The reference chapters describe what exists; this guide describes what to pick when you have no specific reason to do otherwise.

It is organised in two parts, which are deliberately different in tone:

  • Part A — The Default Toolbox is framework-universal. These are recommendations that hold for essentially any mORMot 2 project. Treat them as firm defaults: prefer the mORMot type unless you have a concrete reason not to.
  • Part B — A Possible Application Blueprint is one proven way to structure a service, not a framework rule. It is a coherent, batteries-included starting point. Adopt it wholesale for a new service, or cherry-pick what is applicable to your project.

This is a hub document. Each topic links to the chapter that covers it in depth. If you read nothing else, read the two cheat-sheet tables in A.1 and A.2.


Part A — The Default Toolbox

The single most useful thing to internalise: mORMot ships its own data types, and on any code path the framework will serialize, you should use them instead of the RTL counterparts. They reuse storage, leverage the cached RTTI (TRttiCustom/PRttiCustom), use pointer-based UTF-8 paths, and serialize to JSON/binary out of the box. The RTL types are general-purpose, allocate more, and have no shared serialization story. See Chapter 4: Core Units for the units these live in.

A.1. The one-screen cheat sheet

When you reach for a classic RTL construct, this is the default mORMot replacement:

You want to… Use (default) Instead of (RTL) Unit
Hold text on a server code path RawUtf8 string (UTF-16) mormot.core.base
Hold binary data RawByteString / TSQLRawBlob TBytes mormot.core.base
List-like ops over array of T (Add/Sort/Find) TDynArray (wraps your array) TList / TList<T> mormot.core.data
O(1) lookup over an existing array TDynArrayHashed / TDynArrayHasher TDictionary<K,V> mormot.core.data
Typed generic list/dict with serialization IList<T> / IDict<K,V> (Collections.New…) Generics.Collections.TList<T> mormot.core.collections
Schema-less JSON object/array TDocVariant / TDocVariantData Variant / System.JSON.TJSONObject mormot.core.variants
Schema-less doc with a clean, typed API IDocList / IDocDict raw Variant late binding mormot.core.variants
Build text/JSON/CSV/SQL efficiently TTextWriter / TJsonWriter TStringBuilder / Format / concat mormot.core.text / …json
String list (server side) TRawUtf8List TStringList mormot.core.data
Thread-safe key→value dictionary TSynDictionary TThreadList + TDictionary mormot.core.json
Dedupe many identical strings TRawUtf8Interning manual hash table mormot.core.data
Iterate lines of a file zero-copy TMemoryMapText TStringList.LoadFromFile mormot.core.text
Talk to the database IRestOrm (interface) + TOrm descendants concrete TRestOrmServer mormot.orm.core
Copy fields between objects / records / DTOs TRttiMap (AutoMap) — see A.7 hand-written field-by-field copy mormot.core.rtti

Why not just use the RTL? Full reasoning is in Chapter 4. The short version: the mORMot types serialize automatically across the SOA/REST boundary, carry an interface-managed lifetime (no try..finally Free), keep UTF-8 end-to-end, and are backed by TDynArray/TDynArrayHasher for cache-friendly speed.

A.2. Recommended type at each layer

The framework marshals values as they cross layers. Use the mORMot type on every signature it will touch; RTL types are fine only inside an implementation body where nothing crosses a boundary.

Layer Recommended type Why
HTTP boundary RawUtf8 Zero-copy on the socket buffer; UTF-8 end-to-end, no conversion
API input — schema-less IDocDict / variant Framework recognises it and streams JSON directly
API input — typed packed record DTO (Rtti.RegisterFromText required per DTO on FPC ≤ 3.2.2; Delphi only when needed — see A.7) Compile-time field checking; cheapest carrier (value type)
Service interface signature simple types, records-with-RTTI, TOrm descendants, array of …, variant, IDocList/IDocDict, IList<T>/IDict<K,V> Only these serialize automatically across the SOA boundary
Business logic — typed collections IList<T> / IDict<K,V> Interface lifetime, built-in JSON/binary, cache-friendly
Business logic — schema-less IDocList / IDocDict Same TDocVariantData core, strongly-typed surface
Hot-path raw storage TDynArray over array of T No class wrapper; external count → amortised O(1) Add
Hot-path hashed lookup TDynArrayHashed In-place hashing over the same storage; no parallel container
DB access IRestOrm + TOrm descendants Swappable backend, mockable, RTTI-driven SQL
JSON output TTextWriter / TJsonWriter O(1) allocations, RTTI-driven; never use concat or Format
String list (server) TRawUtf8List UTF-8 native, thread-safe (fThreadSafe), O(1) IndexOf (fNoDuplicate)
String list (UI binding) TStringList Keep where VCL/LCL TStrings forces it; convert at the boundary

Practical rule for service interfaces: parameters and results of an IInvokable service should be one of simple types, records-with-RTTI, TOrm descendants, array of …, variant/TDocVariantData, IDocList/IDocDict, or IList<T>/IDict<K,V>. Reach for RTL generics only inside the implementation, never on the signature. See Chapter 16: Services via Interfaces.

A.3. Replacing TStringList

TStringList is the most-overused RTL class in ported code. The default replacement is TRawUtf8List — thread-safe, UTF-8 native, O(1) IndexOf with fNoDuplicate:

list := TRawUtf8List.CreateEx([fCaseSensitive, fNoDuplicate, fThreadSafe]);
try
  list.Add('foo');
  if list.IndexOf('foo') >= 0 then ... // O(1) with fNoDuplicate
  list.Values['key'] := 'value';       // name=value pair
finally
  list.Free;
end;

Pick by what you actually used TStringList for:

Used TStringList for… Better mORMot type
Plain UTF-8 string list TRawUtf8List
Typed string list w/ serialization IList<RawUtf8>
Name=value config TRawUtf8List (Name/Value) or TDocVariantData for nested
Thread-safe key→value dictionary TSynDictionary
Typed key→value w/ generics IDict<K,V>
Schema-less JSON-like dict IDocDict / TDocVariantData
Parsing CSV/lines into tokens CsvToRawUtf8DynArray, TSynTempBuffer
Building delimited text TTextWriter.AddCsvUtf8 / AddString

Keep TStringList only where VCL/LCL UI controls (TListBox.Items, TMemo.Lines) require a TStrings, or in working code that isn't on a hot path. Convert to RawUtf8 at the boundary.

A.4. Service instance mode — default to sicShared

When you register an interface-based service, you choose an instance lifetime. The default:

Is the service stateless / read-mostly (validators, calculators, cache-fronted readers)?
        │
        ├── Yes ──► sicShared   (one instance, all threads — your code MUST be thread-safe)
        │
        └── No ──► Does it hold EXPENSIVE thread-affine state
                   (dedicated DB connection, prepared-statement cache,
                    crypto context, non-thread-safe library)?
                        │
                        ├── Yes ──► sicPerThread
                        │
                        └── No ──► Is the state genuinely conversational
                                   (a multi-call workflow)?
                                        │
                                        ├── Yes ──► sicPerSession / sicClientDriven
                                        │
                                        └── No ──► sicShared + a keyed cache for per-user data

Thread-safety rules that hold in every mode: a TOrm instance is never shared between threads (create one per call); a TRestBatch is never shared (build it as a local inside the method); IRestOrm reads are thread-safe; writes serialize at the storage layer. For shared mutable state in a sicShared service, guard it with TSynLocker, TRWLock, or TSynDictionary. See Chapter 16.

A.5. Bind to IRestOrm, and the _Safe() rule

Two habits that prevent a large class of bugs:

  • Store IRestOrm (the interface), not TRestOrmServer (the class). This is what makes the storage backend swappable, mockable in tests, and friendly to dependency injection. Cast up to IRestOrmServer only at the composition root, when you need server-only methods like CreateMissingTables.
  • Always go through _Safe(v) before reinterpret-casting a variant to TDocVariantData. _Safe walks varByRef indirection and returns a sentinel empty record if the value isn't a TDocVariant. A direct TVarData cast on a null or RTL variant corrupts memory.
with _Safe(v)^ do        // safe even if v is null / not a TDocVariant
  email := U['email'];

A.6. Minimum recommended server configuration

A sane default server wires a handful of deliberate decisions: a model, a storage server bound as IRestOrm, your services, authentication, an HTTP server, and logging. Every one of these is a named decision — none should be left implicit.

Two independent axes: logical layering vs physical topology

Before choosing a server layout, separate two decisions that are easy to conflate:

  • Logical — how your services are layered: repository, CQRS read/write split, event-driven handlers, DDD use-cases. This is a code-organisation concern (it is what Part B is about), and it is independent of where anything runs.
  • Physical (topology) — how many TRestServer / executable instances exist, and which of them are reachable from the network.

These axes are orthogonal because mORMot gives you location transparency: an IInvokable service is consumed identically whether its implementation lives in-process or on another machine. The call site never changes:

var
  svc: IUserQuery;          // the service interface; same type, local or remote
  profile: TUserProfile;
begin
  // Resolve once. Local: from the server's own container.
  // Remote: from a client pointed at the other instance.
  if Rest.Services.Resolve(IUserQuery, svc) then
    // From here the call site is identical, wherever svc actually runs:
    profile := svc.GetProfile(userID);

Switching a service from local to remote is a matter of instance initialization — register it on a different server, point a client at a URI — not a code change. The layering you design in Part B therefore survives any topology you later deploy it on.

Make the switch a localized decision with the resolver pattern. Both TRestServer.Services and TRestClientURI.Services are TInterfaceResolver descendants (mormot.core.interfaces), so the local-vs-remote choice collapses to which resolver instance the composition root hands out. Consumer code depends on one global TInterfaceResolver (or, better, is itself a TInjectableObject whose published interface properties are auto-resolved — the same mechanism B.5 recommends inside services) and never learns where the implementation runs:

unit AppUserClient;   // the only unit consumer code depends on

interface

uses
  mormot.core.interfaces;

function UserResolver: TInterfaceResolver;                 // read — for consumers
procedure SetUserResolver(aResolver: TInterfaceResolver);  // write once — for the wiring unit

implementation

var
  fUserResolver: TInterfaceResolver;   // private: nobody can reassign it directly

function UserResolver: TInterfaceResolver;
begin
  if fUserResolver = nil then
    raise EServiceException.CreateUtf8(
      'UserResolver read before wiring — link AppUserClientLocal or ...Remote', []);
  result := fUserResolver;
end;

procedure SetUserResolver(aResolver: TInterfaceResolver);
begin
  if fUserResolver <> nil then
    raise EServiceException.CreateUtf8(
      'UserResolver already set — link exactly one wiring unit', []);
  fUserResolver := aResolver;
end;

end.

The variable stays private in the implementation section, so the contract is write once, read many: the read accessor raises if something reads it before a wiring unit ran, and SetUserResolver raises on a second assignment — turning the link-two-wiring-units mistake into a loud startup failure instead of a silent last-write-wins. Each wiring unit below uses AppUserClient and calls SetUserResolver in its initialization.

The two wiring units differ only in what they assign to that variable — an in-process server's container locally, an HTTP client's container remotely:

AppUserClientLocal.pas — builds the whole layered stack in-process (the A.6.1 monolith)
unit AppUserClientLocal;

implementation

uses
  AppUserClient;   // the shared UserResolver / SetUserResolver

var
  Rest: TRestServerDB;

initialization
  Rest := TRestServerDB.Create(TOrmModel.Create([TOrmUser]), ':memory:'); // or 'data.db'
  Rest.Model.Owner := Rest;
  Rest.Server.CreateMissingTables;
  // domain/persistence layer owns the ORM; app layer gets it injected (B.5).
  // CreateInjected's 3 arrays are [stubsByGuid], [otherResolvers], [dependencies]:
  Rest.ServiceDefine(
    TUserService.CreateInjected([], [], [TUserPersistence.Create(Rest.Orm)]),
    [IUserQuery, IUserCommand]);
  // hand out the server's own container — Resolve() returns direct references:
  SetUserResolver(Rest.Services);
finalization
  FreeAndNil(Rest);
end.
AppUserClientRemote.pas — an HTTP client to the standalone server (the A.6.2 distributed layout)
unit AppUserClientRemote;

implementation

uses
  AppUserClient;   // the shared UserResolver / SetUserResolver

var
  Http: TRestHttpClientSocket;

initialization
  // host/port/root come from your settings file — deployment config, not code
  Http := TRestHttpClientSocket.Create(
    Settings.Server, Settings.Port, TOrmModel.Create([], Settings.Root));
  Http.Model.Owner := Http;
  // declare the SAME interfaces, this time as client-side factories:
  Http.ServiceDefine([IUserQuery, IUserCommand], sicShared);
  // hand out the client's container — Resolve() returns HTTP proxies:
  SetUserResolver(Http.Services);
finalization
  FreeAndNil(Http);
end.
// Project.dpr — the ONE place topology is chosen:
uses
  AppUserClient,
  AppUserClientLocal;      // <-- swap to AppUserClientRemote for distributed

// consumer code, identical and unaware of the line above:
if UserResolver.Resolve(IUserQuery, svc) then
  svc.GetProfile(userID, profile);

Local is the A.6.1 monolith (one exe, full stack in-process); Remote is the A.6.2 distributed layout (HTTP client to a standalone server). The business code is identical; only the linked unit differs. And inside a service implementation you usually don't even touch UserResolver directly — a TInjectableObject descendant publishes property Users: IUserQuery and the resolver chain fills it (B.5).

Both units self-wire in their initialization, so linking one is genuinely all you do; the only extra need of the remote unit is the host/port/root in your settings file — deployment configuration, not code. (Wiring in initialization does mean the settings must be loadable at unit-initialization time; if you want explicit startup control, move those few lines into your daemon's startup instead.)

IMPORTANT — select the topology with a define (`ifdef`)

Centralize the choice in an include file: e.g. project.inc may contain {$DEFINE LOCALMORMOT}:

// AppUserClient.pas — selects the implementation at compile time
{$I project.inc}   // defines LOCALMORMOT for the local build, nothing for remote

uses
  {$ifdef LOCALMORMOT}
  AppUserClientLocal,
  {$else}
  AppUserClientRemote,
  {$endif}
  ...;

Now the single switch lives in project.inc — place {$define LOCALMORMOT} in project.inc and the AppUserClientLocal is used.

A single-executable monolith can still be a fully layered internal stack of services. You do not split processes to get clean layering — you get that logically, inside one exe. You split processes only when scale, isolation, or a public network boundary actually demands it. So pick the physical topology by deployment context, not by how layered you want the code to be:

  • A.6.1 — a single server, the right default for a trusted / local deployment (one exe, still logically layered).
  • A.6.2 — a private system-of-record plus a public edge, for an Internet-facing or distributed deployment.

A cross-cutting rule that applies to both. Topology reduces exposure; it never substitutes for authorization. Scope every read to the caller: a logged-in end user must retrieve only their own rows, never the whole table. This is a service-design concern (filter each query to the session's identity at the service boundary) — no choice of server count fixes it. See B.6.

A.6.1. Trusted / local deployment

For a service that is only reachable on a trusted network (local box, corporate LAN, in-cluster), a single server is correct and simplest. The ORM REST routes it exposes (/root/TUser, …) are not an unguarded second door: they sit behind the same session authentication as your services, and behind TOrmAccessRights, which lets you make a table read-only, forbid CRUD per-table, or expose none at all.

// 1. Logging: turn it on early (see Chapter 25)
TSynLog.Family.Level := LOG_VERBOSE;
TSynLog.Family.PerThreadLog := ptIdentifiedInOnFile;

// 2. Model: list every TOrm class the server persists
Model := TOrmModel.Create([TUser, TInvoice]);

// 3. Storage: embedded SQLite is the default system of record
RestServer := TRestServerDB.Create(Model, 'data.db');
RestServer.Server.CreateMissingTables;       // server-only call, at composition root

// 4. Services: register interface-based services (default sicShared)
RestServer.ServiceDefine(TUserService, [IUserQuery, IUserCommand], sicShared);

// 5. Authentication: enable it explicitly (JWT / mORMot auth)
RestServer.AuthenticationRegister(TRestServerAuthenticationDefault);

// 6. Access rights: tighten or disable the ORM REST routes you do not want
//    exposed (per-table CRUD bits, per TAuthGroup); services stay reachable.
//    See Chapter 21, 21.9.1.
Rights := SUPERVISOR_ACCESS_RIGHTS;
Rights.Edit(Model, TInvoice, {C=}false, {R=}true, {U=}false, {D=}false); // read-only
Group.OrmAccessRights := Rights;     // applied to the TAuthGroup row

// 7. HTTP: expose over HTTP/HTTPS
HttpServer := TRestHttpServer.Create('8080', [RestServer], '+', HTTP_DEFAULT_MODE);

Adjust per project: swap TRestServerDB for an external SQL/NoSQL backend behind the same IRestOrm abstraction (see A.5 and B.1); choose your authentication scheme in Chapter 21; tune logging in Chapter 25.

A.6.2. Internet-facing or distributed deployment

When the edge is public on the Internet, or when you are orchestrating several service instances across a server network, split the topology to minimise the public surface; the gain is reduced exposure and a clean place to terminate TLS/JWT.

  • A private system-of-record (TRestServerDB, SQLite or external SQL) that owns the business tables and the auth records, and is never handed to an HTTP server — so it has no network surface and is reachable only in-process. It still publishes services (repository / CQRS, authentication, the main data model); "private" means not network-exposed, not data-only.
  • A public edge (TRestServerFullMemory, void model) that terminates TLS, validates JWT, and exposes only the context-scoped services each application actually needs. The edge consumes the private services either in-process (same instance) or over the network (distributed).

The edge coding is an addition to A.6.1, not a different world:

// === PRIVATE: the system of record. Owns the data. No network surface. ===
// Same wiring as A.6.1 steps 1-6, but the resulting server is NOT passed to
// any TRestHttpServer: it stays reachable only in-process.
DbModel  := TOrmModel.Create([TUser, TInvoice, TAuthUser, TAuthGroup]);
DbServer := TRestServerDB.Create(DbModel, 'data.db');
DbServer.Server.CreateMissingTables;
// (register the private services on DbServer here: repository/CQRS, auth, ...)

// === PUBLIC: the edge. Owns no data. Only context-scoped services + JWT. ===
// Void model => no TOrm classes => no ORM REST routes can exist at all.
EdgeModel  := TOrmModel.Create([]);
EdgeServer := TRestServerFullMemory.Create(EdgeModel);

// Inject the private DB ORM explicitly at the composition root: the service
// runs on EdgeServer, whose inherited Server.Orm is the void edge ORM, so bind
// the private ORM here rather than relying on Server.Orm (the explicit form of
// B.5's injection). Publish only the services this application context needs.
EdgeServer.ServiceDefine(
  TUserService.Create(DbServer.Orm),          // inject the private DB ORM
  [IUserQuery, IUserCommand]);                // registered as sicShared

// Only the edge is handed to HTTP — the private server stays off the network.
HttpServer := TRestHttpServer.Create('443', [EdgeServer], '+', HTTP_DEFAULT_MODE);

Publishing too much is the classic Internet-facing mistake; the cross-cutting rule from A.6 — scope every read to the caller — is what actually prevents data leaks here. See B.6.

A.6.3. Mapping to standard architecture vocabulary

The above wording has been picked on purpose — here is how it maps to the established literature, along the same two axes.

Physical / topology axis (A.6.2 — the network boundary):

Term used here Established name Reference
Public edge API Gateway; Backend-for-Frontend (BFF) when one edge serves one client app Richardson, Microservices Patterns [8]; Newman, Building Microservices [6]
Private system-of-record System of Record (SoR); the core/domain side of a Hexagonal (Ports & Adapters) boundary Cockburn, Hexagonal Architecture [1]
Private/public split an inbound adapter (the edge) in front of the core — Ports & Adapters applied at the deployment boundary Hexagonal [1] / Onion architecture [7]

Logical / layering axis (Part B — in-process, independent of topology):

Term used here Established name Reference
Application layer / context-scoped services use cases / application services Evans, DDD [2]; Martin, Clean Architecture [5]
Repository interface (B.1) Repository pattern Evans, DDD [2]; Fowler, PoEAA [3]
Query/Command split (B.4) CQRS Young [9]; Fowler [4]
Domain object carried on the TOrm (B.3) entity / aggregate Evans, DDD [2]

Two caveats, because it is tempting to file all of this under one label such as "Clean Architecture":

  • Clean Architecture [5] / Onion [7] / DDD [2] describe the logical axis — in-process layering — not how many processes you run. They apply just as much to the A.6.1 monolith as to A.6.2.
  • API Gateway / BFF [8] / hexagonal-edge [1] describe the physical axis — the network boundary. They are what A.6.2 adds on top of the layering.

A.7. Copying between objects, records and DTOs

The most repetitive chore in a layered mORMot app is moving the same fields between a TOrm and its DTO, by hand, one assignment at a time. It is boring, and it fails silently: a field you forget to copy is not a compile error, it is a bug. The framework already solves the common case with RTTI, so don't hand-write a 1:1 copy — declare the mapping once and let mORMot run it.

The default: TRttiMap (mormot.core.rtti). It is a small value type you set up once — typically a global in your DTO unit — and then reuse on every call. Once Init and Map/AutoMap are done it is thread-safe to share, so one global per entity pair is exactly right for the sicShared services of A.4.

type
  TOrmUser = class(TOrm)        // the ORM, doubling as the domain object (B.3)
  published
    property LogonName: RawUtf8 read fLogon write fLogon;
    property DisplayName: RawUtf8 read fName write fName;
    property YearOfBirth: integer read fYob write fYob;
  end;

  TUserDto = packed record      // its DTO — some field names differ on purpose
    Id: Int64;
    Email: RawUtf8;             // <- LogonName
    Name: RawUtf8;             // <- DisplayName
    YearOfBirth: integer;       // <- same name: AutoMap pairs it for free
  end;

var
  UserMap: TRttiMap;            // built once, shared by all threads

initialization
  UserMap.Init(TOrmUser, TypeInfo(TUserDto)).AutoMap   // match every same-name field
    .Map([                                             // then override the renamed ones
      'RowID',       'Id',                             // a TOrm's key is ID/RowID
      'LogonName',   'Email',
      'DisplayName', 'Name']);

AutoMap pairs the fields whose names already match; the fluid .Map(...) calls fix only the exceptions. A field name of '' means "ignore this field" — handy for never copying a secret or audit column.

From there, three call shapes cover everything:

UserMap.ToB(user, @dto);   // fill an existing DTO   from a TOrm   (ORM -> DTO)
UserMap.ToA(user, @dto);   // fill an existing TOrm  from a DTO    (DTO -> ORM)
dtoPtr := UserMap.ToB(user);          // allocate + fill a NEW DTO from a TOrm
newUser := UserMap.ToA(@dto);         // allocate + fill a NEW TOrm from a DTO
UserMap.ToArrayB(users, dtos);        // map a whole dynamic array at once

Two gotchas are worth knowing before you rely on it:

Gotcha 1 — the primary key. A TOrm's primary key is ID/RowID. If your DTO calls it anything else (Id, UserId, …), pair it explicitly — 'RowID', 'Id' — because AutoMap won't match different names. If your DTO field is literally RowID, AutoMap handles it and you can drop that line.

Gotcha 2 — enums copy by ordinal, not by name. TRttiMap copies an enum field by its integer ordinal (a direct value copy when both sides share the enum type; via an integer otherwise) — it never matches enum values by name. So if a DTO and a TOrm use two different enum types whose members are declared in a different order, the copy is silently wrong: esActive could land as esDeleted. Keep both sides on the same enum type (the natural choice when the TOrm doubles as the domain object, B.3), or — if the enums genuinely diverge — map that one field by hand and leave the rest to TRttiMap.

Gotcha 3 — Pascal casing doesn't travel; pick a JSON convention. TRttiMap copies between Pascal identifiers in memory, so casing never matters to ToA/ToB. The moment a DTO crosses the wire as JSON it does: mORMot serializes field names exactly as declared, so TUserDto.Email/YearOfBirth go out as {"Email":…,"YearOfBirth":…} — PascalCase, which most web consumers don't expect. Normalize it once, in the initialization of the unit that declares the DTO (so every server, client, and consumer that links the type gets the same wire contract), and on the DTO type, never the TOrm — NameChangeCase mutates that type's shared RTTI process-wide, and you don't want to reshape every ORM payload: Rtti[TypeInfo(TUserDto)].Props.NameChangeCase(scLowerCaseFirst) turns every field camelCase (email, yearOfBirth) — the default convention for new web APIs, and the one mORMot's own docs flag as "aka camelCase, for API." On FPC, place it after the Rtti.RegisterFromText those record DTOs already need (the fields don't exist to rename until registration builds them; on Delphi the order is irrelevant). Reach for scSnakeCase (year_of_birth) instead only when the surrounding system already standardized on it (Rails/Django backends). If just a field or two deviate, alias them by hand with Props.NameChanges(['Email','contactEmail']); pass NestedEnums to NameChangeCase when enum member names must follow the same convention on the wire (see Gotcha 2).

Compiler caveat — Delphi needs no RegisterFromText; FPC (≤ 3.2.2) does. On Delphi 2010+, records expose field-level RTTI (names and types) natively — all AutoMap and JSON serialization need — so a plain DTO needs no Rtti.RegisterFromText. FPC differs: through the current stable FPC 3.2.2, standard record RTTI does not expose field names, so mORMot can neither map nor serialize a record DTO by name without help. Verified on FPC 3.2.2 + mORMot2: a record DTO with no Rtti.RegisterFromText serializes to an opaque binary base64 blob ("￰BwAAAA…"), not {"Title":…}. So on FPC, declare Rtti.RegisterFromText(TypeInfo(TUserDto), '…') in initialization for every record DTO you map or serialize. (FPC trunk/3.3.1 extended RTTI may lift this — unverified here; assume the 3.2.2 baseline.) On Delphi, add RegisterFromText only when (a) a DTO field hits a Delphi RTTI gap — typically a static array of an unmanaged type, where Delphi emits no field RTTI — or (b) you want custom or guaranteed cross-platform serialization. On both compilers, a dynamic array of an already-registered record element needs nothing extra (the element's RTTI drives it).

When to use something else

TRttiMap is the default for a stable, mostly 1:1 mapping. Reach for another routine when the shape is different:

Situation Use instead Why
The mapping is logic, not a copy — joins several tables, computes/derives fields, redacts conditionally (the reduced DTOs of B.8) a hand-written mapper procedure This is real domain code; it deserves to be explicit and tested, not hidden in a map config
A one-off / rare copy where a persistent map isn't worth it RecordToObject / ObjectToRecord / CopyObject One-shot name-matched copy, no setup object
You are already producing JSON anyway (e.g. the response body) ObjectToJson → RecordLoadJson / ObjectLoadJson The round-trip is essentially free once you're in JSON
The target is schema-less / dynamic, or you want to pick fields out by name ObjectToVariant / DocVariantToObject Bridges to a TDocVariant you can read/compose loosely
A same-type copy (no mapping at all) RecordCopy / CopyObject No name matching needed

Test the mapping, because name-matching is silent

AutoMap (like the JSON round-trip and CopyObject) matches by name at startup. Rename a field on one side only and that field silently stops copying — no error. TRttiMap ships the antidote: Compare checks two instances field-by-field, and RandomA / RandomB fill an instance with random values. A three-line round-trip test (RandomA → ToB → Compare = 0) catches drift the moment it happens, and is the cheapest, highest-value test you can write — see B.6. See Chapter 4: Core Units for the units these routines live in, and Chapter 5: ORM for the TOrm side.


Part B — A Possible Application Blueprint — Deviate Where Required

This section is not about a framework rule. It is about a possible blueprint Use it as a possible starting point. It is a coherent default for a new service in a non-trivial codebase. You can adopt it and deviate where your domain differs. It consolidates the SOA/DDD guidance from Chapter 15, Chapter 16, and Chapter 24.

Worked example. The recommended-patterns sample is a full, compilable application that follows this blueprint end to end: a DDD + CQRS task manager built with FPC, hosting its own SQLite3 database with FTS5 full-text search, exposing interface-based Query/Command services over SOA, and organised as dom/ infra/ app/ feature modules with interchangeable in-process and HTTP client backends. The §/A.x/B.x references throughout its docs point back to this guide.

B.1. Storage behind an interface

Every service keeps its storage behind an IXxxRepository in the domain layer; the service's CQRS pair (B.4) belongs to the application layer — see B.6.1. Multi-tenancy: per-tenant SQLite when data is tenant-scoped; a single shared database otherwise.

B.2. ORM base class and casing

  • Use TOrm everywhere; migrate legacy TSQLRecord holdouts opportunistically.
  • Use the modern casing RawUtf8 (not RawUTF8) — aliases, but lowercase is the current convention.
  • Behavioural cost of both: zero. It is search-and-replace.

B.3. Two types per entity (not three)

  • A TOrm class that is also the domain object — Equals, validation, and computed properties live on it directly. No separate shadow class.

Carrier choice — stay with packed record when possible: cheapest carrier (value type, no heap, no constructor), serializes via cached RTTI. On Delphi 2010+ a plain DTO of simple fields needs no Rtti.RegisterFromText; on FPC (through 3.2.2) each record DTO does need one for field-named JSON (without it mORMot emits a binary blob — see A.7). Beyond that FPC baseline, register only for the A.7 exceptions (a field alias, custom/guaranteed-cross-platform serialization, or a Delphi RTTI gap such as a static array of an unmanaged type). string is forbidden on the wire — use RawUtf8 for text and RawByteString/TSQLRawBlob for binary. Reach for TSynPersistent/TSynAutoCreateFields when the wire object needs methods or owned nested children. For lists across a SOA boundary, return array of TXxxListItemDTO (lighter) or IList<TXxxListItemDTO> (interface lifetime) — never raw TList<T> or TJSONObject.

Map the 1:1 DTO, don't hand-copy it. When the DTO mirrors the entity field-for-field, copy it with TRttiMap (AutoMap, overriding the renamed fields) instead of writing the assignments by hand — see A.7. Hand-written mappers are for the projection DTOs of B.8, where the copy is real logic.

B.4. CQRS read/write split

Two interfaces per entity by default: IXxxCommand (writes) and IXxxQuery (reads), each descending from IInvokable. The "entity" here may be logical rather than physical — an aggregate in DDD terms: IOrderCommand.Save may persist several database tables plus files on disk behind one method.

Why: per-direction authorization (read-only clients can't reach write methods); the read side can be served by a replica or cache without touching writes; the interfaces become self-documenting. Acceptable to skip for genuinely trivial wrappers (one or two endpoints, no read/write asymmetry) — but skip it as an exception, not as the rule.

B.5. SOA composition root and dependency injection

  • IRestOrm is the binding type in business code; cast to IRestOrmServer only at the composition root for server-only methods.
  • Inject via TInjectableObjectRest / TInjectableObject: publish interface properties and let AutoResolve fill them through the TRestServer.Services resolver chain, or use the inherited Server.Orm. Concrete-class fields cannot be auto-injected — interfaces only.
  • Seed the injector with CreateInjected. Its signature is CreateInjected(aStubsByGuid, aOtherResolvers, aDependencies). Pass the concrete dependency instances a service needs — typically its repositories — in the third array, and leave the first two empty outside tests (the first auto-stubs interfaces by TGuid, the second chains in extra TInterfaceResolvers). CreateInjected is not an alternative to AutoResolve — it seeds the resolver with those instances and then calls AutoResolve itself, so the instances you pass are exactly what gets bound to the published interface properties. Injected TInterfacedObject dependencies are reference-counted and released when the service is destroyed.
  • Instance mode: default sicShared, promote only when justified — see A.4.

B.6. Cross-cutting standards

  • Folder layout. Keep the persistence boundary visible: lowercase dom/, infra/, app/, tests/, one subfolder per entity, with the daemon and client units in serv/ and at the source root. Split each app/<entity>/ into interface/ (the CQRS interfaces and DTOs clients compile against) and implementation/ (server-side services and mappers). Don't merge domain and infrastructure into one folder. See B.6.1 for the full tree.
  • Result handling. A shared base enum (e.g. TServiceResult: success / invalid-request / not-found / denied / db-error) plus optional per-service extensions — stops the proliferation of near-identical result enums.
  • Auth on the consumer side. Extract a single guard helper so every service does one Guard.Require(Token, …) call instead of a repeated multi-step token ritual.
  • Scope every read to the caller. The most common and most damaging public-service mistake is publishing too much: letting a logged-in user retrieve all rows when they should see only their own. Filter every query to the session's identity at the service boundary. No server-topology choice (see A.6) substitutes for this.
  • Tests. A tests/ folder per service; mapping is the cheapest, highest-value test target — whether a TRttiMap (round-trip via Compare / RandomA, see A.7) or a hand-written projection mapper. Exercise it first.

B.6.1. Suggested folder structure

The layout below realises the layering of B.1–B.5 on disk. One subfolder per entity inside each layer, plus a serv/ folder for the daemon and the topology-selecting client units of A.6. <entity> is the lowercase entity name; <Name> is the service name.

src/
├── dom/                          # domain: the TOrm entity (B.3) + its repository port
│   └── <entity>/                 #   uses mormot.orm.core for the TOrm base type — never IRestOrm
│       ├── <entity>.pas               # TOrm aggregate = the domain object (B.3)
│       └── <entity>_repository.pas    # I<Entity>Repository — persistence port (B.1)
├── infra/                        # infrastructure: IRestOrm runtime, SQL, FTS5, migration
│   └── <entity>/
│       └── <entity>_repository_orm.pas # T<Entity>RepositoryOrm implements I<Entity>Repository;
│                                       #   owns schema migration and all SQL
├── app/                          # application layer: CQRS contract, DTOs, mappers, service impls (B.8)
│   └── <entity>/
│       ├── interface/                 # public contract — shared by the server and its clients
│       │   ├── <entity>_query.pas          # I<Entity>Query   — CQRS read  (IInvokable) (B.4)
│       │   ├── <entity>_command.pas        # I<Entity>Command — CQRS write (IInvokable)
│       │   └── <entity>_dtos.pas           # packed-record DTO family (Rtti.RegisterFromText on FPC)
│       └── implementation/            # server side only (and the A.6.1 in-process stack)
│           ├── <entity>_mappers.pas        # pure OrmTo* / *ToOrm / Apply* — cheapest test target
│           ├── <entity>_query_impl.pas     # T<Entity>QueryService  (TInjectableObjectRest, sicShared)
│           └── <entity>_command_impl.pas   # T<Entity>CommandService
├── serv/app/
│   ├── ServApp<Name>.pas         # the daemon / composition root: build the TOrmModel, wire
│   │                             #   repositories, ServiceDefine, start TRestHttpServer (B.7)
│   └── <name>serverstart.pas     # entry point — runs ServApp<Name> as a TSynDaemon
├── App<Name>Client.pas           # the one unit consumer code depends on (A.6)
├── App<Name>ClientLocal.pas      # in-process stack — the A.6.1 monolith
└── App<Name>ClientRemote.pas     # HTTP client      — the A.6.2 distributed layout
tests/
└── <entity>/
    └── <entity>_tests.pas        # one TSynTestCase per entity; start with mapper round-trips

The TOrm lives in dom/, on purpose. Because the TOrm doubles as the domain object (B.3), it belongs with the domain, not with the persistence machinery. The cost is precise and acceptable: dom/ depends on the ORM base type (mormot.orm.core) but never on the ORM runtime — no IRestOrm, no REST server, no SQL. Those stay in infra/. This keeps the dependency direction clean: the port interface in dom/ can name the entity it returns without reaching into infra/, and infra/ depends on dom/, never the reverse. (Some older code merges the two into a single kdom/-style folder — that is the case this split is meant to avoid.)

The CQRS interfaces live in app/<entity>/interface/, not in dom/. I<Entity>Query / I<Entity>Command take and return the DTOs, so declaring them in dom/ would make the domain depend on the application layer — the inverted direction this layout exists to prevent. They are the application layer's public contract, the use-case boundary clients call (a driving port), whereas I<Entity>Repository is a port the domain needs (a driven port) and stays in dom/. Consumer code compiles against interface/, never against dom/ or implementation/, which are linked only by the server and the A.6.1 in-process stack.

Pattern Layer Role
<entity>.pas dom TOrm aggregate — the domain object (B.3)
<entity>_repository.pas dom I<Entity>Repository persistence port
<entity>_repository_orm.pas infra IRestOrm-backed repository + schema migration + SQL
<entity>_query.pas app/interface I<Entity>Query — CQRS read interface
<entity>_command.pas app/interface I<Entity>Command — CQRS write interface
<entity>_dtos.pas app/interface packed record DTO family
<entity>_mappers.pas app/implementation pure OrmTo* / *ToOrm / Apply*
<entity>_query_impl.pas app/implementation T<Entity>QueryService
<entity>_command_impl.pas app/implementation T<Entity>CommandService
ServApp<Name>.pas serv daemon / composition root
<name>serverstart.pas serv entry point — runs the daemon as a TSynDaemon
App<Name>ClientLocal.pas / …Remote.pas (root) topology selection (A.6)
<entity>_tests.pas tests one TSynTestCase per entity

The first rule the layout exists to enforce: app/<entity>/implementation/*_impl.pas depends on the dom/ repository ports and its own interface/ contract, never on infra/*_repository_orm.pas. serv/ is the only place that names the concrete T<Entity>RepositoryOrm class — it constructs each repository, registers it for injection, and hands the resolver to the services (B.5, B.9). Swapping SQLite for another backend, or a stub for tests, is an infra/ change that nothing in app/ or dom/ sees.

The second rule: app/<entity>/interface/ uses no dom/ unit. When a DTO needs a type the aggregate also has — a record embedded in the TOrm, for instance — the DTO unit declares its own record with the same fields, and <entity>_mappers.pas copies between the two with TRttiMap (A.7). The wire contract then does not change when the domain does, and a remote client links no domain unit. Keep the dom/ and infra/ folders off the remote client's search path, so a violation fails the build.

B.7. Default starting point for a new service

The following list is a possible starting point. It is not a pattern to always follow:

  1. TOrm per entity (in dom/), doubling as the domain object — see B.6.1.
  2. A family of (possibly packed record) DTOs (in app/<entity>/interface/) — register each with Rtti.RegisterFromText in the unit's initialization on FPC (required through 3.2.2); on Delphi only for the A.7 exceptions (field aliases, custom serialization, or a Delphi RTTI gap).
  3. Possibly pairing IXxxCommand + IXxxQuery interfaces descending from IInvokable (in app/<entity>/interface/).
  4. Service implementations as TInjectableObjectRest subclasses, IRestOrm injected via published property.
  5. sicShared instance mode unless profiling says otherwise.
  6. Standard daemon: load settings, build the TOrmModel, create the persistence server, wire repositories, register services, start TRestHttpServer.
  7. A single auth guard consuming tokens from the shared auth service.
  8. RawUtf8 everywhere data crosses a boundary; string only where VCL/LCL forces it.
  9. A tests/ folder, starting with mapper round-trip tests (A.7's Compare / Random for TRttiMap, direct assertions for projection mappers).

B.8. The Application layer — expose commands and queries, not entities

Sections B.1–B.7 describe the entity-level machinery: TOrm classes, DTOs, command/query pairs, the daemon. This section covers what clients are allowed to call. Publishing the schema as services invites over-fetching. The application layer is the deliberate alternative, and it is one rule with two halves:

  1. Command/query pairs belong to the edge. Everything the network can reach is an IXxxQuery/IXxxCommand pair (B.4) — but scoped by client workflow, not by physical table. The "entity" of such a pair is the logical aggregate that B.4 already allows: ICheckoutCommand.Confirm may persist order rows, invoice rows and files behind one method.
  2. The internal server is free from this rule. Behind the edge, the implementation works directly on TOrm through IRestOrm — no interface ceremony required there. Business rules keep their one home: the TOrm class doubling as the domain object (B.3).
        mobile app / web client / desktop client
                        │
                        │  HTTPS — the only boundary a client can cross
                        ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ EDGE — the A.6.2 public server (void model, TLS, JWT)       │
 │                                                             │
 │   IXxxQuery   IXxxCommand                                   │
 │   workflow-scoped pairs + reduced DTOs = the app layer      │
 └────────────────────────────┬────────────────────────────────┘
                              │  IRestOrm — in-process,
                              │  no network surface
                              ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ INTERNAL — the A.6.1-style TRestServerDB (data + TOrm)      │
 │                                                             │
 │   free ORM usage: TOrm, TRestBatch, SQL — no interfaces     │
 └─────────────────────────────────────────────────────────────┘

The exposure rule is mechanical: a service is reachable from the network only if it is registered on a server handed to TRestHttpServer. The internal server never is, so its TOrm world simply has no address a client could call.

Design the edge from the client inward, not from the schema outward:

  1. Walk the actual client needs — each screen, each step of the client workflow.
  2. For each step in a workflow that requires the server, define a query or command (ICheckoutQuery/ICheckoutCommand), roughly one method per step. Each method is a use case: named after what the user is doing.
  3. For each screen, define a reduced DTO — a projection carrying exactly the fields that screen needs. Unlike the per-entity DTO family of B.3, a screen DTO may join several entities; composing it from the loaded TOrm instances is a pure mapper procedure (no I/O inside — the service gathers the inputs first). Because this mapper is logic — joining, projecting, sometimes computing fields — hand-write it; TRttiMap (A.7) is for the field-for-field mirror DTO of B.3, not for this.
type
  // reduced DTO: exactly what the checkout screen shows — possibly joining
  // several TOrm tables; not a mirror of any single entity
  TCheckoutSummaryDto = packed record
    OrderRef: RawUtf8;
    Lines: array of TCheckoutLineDto;
    Total, Vat: currency;
    PaymentMethods: array of RawUtf8;
  end;

  // one workflow, one pair; one step, one method
  ICheckoutQuery = interface(IInvokable)
    ['{1E8A39C4-5B7D-4F2A-9C61-D0B5A8E37F12}']
    function GetSummary(const cartID: RawUtf8;
      out summary: TCheckoutSummaryDto): TServiceResult;
  end;
  ICheckoutCommand = interface(IInvokable)
    ['{6F2D81B0-3A9E-4C57-B8D4-72E1C5F90A3B}']
    function Confirm(const cartID, paymentMethod: RawUtf8;
      out orderRef: RawUtf8): TServiceResult;
  end;

The wiring is exactly A.6.2 with the names filled in:

// INTERNAL: data + TOrm, free ORM usage — never handed to TRestHttpServer
DbServer := TRestServerDB.Create(
  TOrmModel.Create([TOrmOrder, TOrmOffer, TOrmInvoice]), 'data.db');
DbServer.Server.CreateMissingTables;

// EDGE: only the workflow pairs are network-reachable
EdgeServer.ServiceDefine(
  TCheckoutService.Create(DbServer.Orm),   // the body works on the ORM directly
  [ICheckoutQuery, ICheckoutCommand]);
HttpServer := TRestHttpServer.Create('443', [EdgeServer], '+', HTTP_DEFAULT_MODE);

The edge service publishes interface properties that the resolver chain fills at creation (B.5), and thanks to the location transparency of A.6 its body never learns whether they are local objects or HTTP proxies:

 EDGE: ICheckoutCommand.Confirm — one client gesture, one use case
   │
   │ orchestrates via published interface properties,
   │ auto-resolved at creation (B.5)
   ├──► auth service         [own DB]  ─┐
   ├──► orders service       [own DB]   ├─ each may run in-process or as a
   ├──► invoice service      [own DB]   │  stand-alone micro-service; the
   └──► audit-trail service  [own DB]  ─┘  call site never changes (A.6)

B.9. The Two-server approach and its ambiguous ORM

Automatic injection resolves by interface type: it works whenever a type has exactly one implementation in the resolver chain. That holds for your services — there is one IOfferQuery in the system, so resolving it has one correct answer, even when its implementation lives on another server reached through a chained resolver.

IRestOrm is the exception. In the two-server topology of B.8, both servers natively carry an ORM: the private DbServer.Orm with the real tables, and the edge server's own void-model ORM with none. For a service hosted on the edge, the ambient answer — the inherited Server.Orm, or resolution by type — is the void edge ORM: every query would hit an empty in-memory store, silently. "Give me an IRestOrm" is an under-specified question in a two-server process; no resolver can know you meant the private one.

The easy way to solve it: Explicit constructor injection at the composition root — what the B.8 wiring does: TCheckoutService.Create(DbServer.Orm) states which ORM, visibly, in one line. The right default when the dependency is one obvious ORM.

The rule: let every service dependency be injected automatically, because its interface type has a single implementation. The ORM is the one dependency you wire by hand.


When to deviate

Part A is a default you should rarely override on serialized code paths — if you find yourself using TStringList or TDictionary<K,V> on a service signature, that is almost always a mistake. Part B is a blueprint. You may collapse the DTO family to one DTO, skip CQRS, or front a legacy schema directly. That is fine.


References

The works cited in A.6.3 and throughout the text:

  1. Cockburn, Alistair. Hexagonal Architecture (Ports & Adapters). 2005. https://alistair.cockburn.us/hexagonal-architecture/
  2. Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003. https://fabiofumarola.github.io/nosql/readingMaterial/Evans03.pdf
  3. Fowler, Martin. Patterns of Enterprise Application Architecture (PoEAA). Addison-Wesley, 2002. https://sar.ac.id/stmik_ebook/prog_file_file/EFCofwzsj0.pdf
  4. Fowler, Martin. CQRS. 2011. https://martinfowler.com/bliki/CQRS.html
  5. Martin, Robert C. Clean Architecture: A Craftsman's Guide to Software Structure and Design. Prentice Hall, 2017. https://dl.acm.org/doi/10.5555/3175742
  6. Newman, Sam. Building Microservices, 2nd edition. O'Reilly, 2021.
  7. Palermo, Jeffrey. The Onion Architecture. 2008. https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/
  8. Richardson, Chris. Microservices Patterns. Manning, 2018. Pattern catalogue at https://microservices.io/patterns/
  9. Young, Greg. CQRS Documents. 2010. https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf

Navigation

Previous Index Next
Chapter 4: Core Units Index Chapter 5: Object-Relational Mapping