Codegen patterns: choosing a model shape
Table of Contents
1. Summary
Choose the pattern before writing a model. Every pattern here generates a different shape of table, type, message and read, and a task that starts from the wrong one produces work that is thrown away.
The patterns are grouped by the question they answer. What am I authoring, how are the entity's facts held, what does the entity opt into, and what does the wire carry. Each entry names the pattern it is mistaken for, because the mistakes cluster.
The syntax for each is in Codegen input org-file schema reference. This page owns the choice, the schema owns the flags.
2. Detail
2.1. How to choose
- Classify the data first. Is it an entity, a list, a family, a bundle, a package or a boundary artefact? The classification is a closed set and it decides which patterns below are allowed: Data classifications: entity, list, family, bundle, package and artefact. A pattern never chooses the classification.
- Name the data shape next. What is keyed, what varies on its own, what is shared, what is optional, what is many. The shape decides the pattern, and a shape that is not yet decided has no pattern.
- Find the row below that answers the shape.
- If two patterns both seem to fit, the shape is not settled. Settle the shape, or split the work, rather than composing both.
- If no pattern fits, stop. Do not invent a flag combination. Report the gap, because a shape the generator cannot express is a finding, and a hand-composed model that no facet consumes generates nothing.
2.2. What am I authoring
| Pattern | Use when | Generates |
|---|---|---|
ores.codegen.entity |
a table with a key, a lifecycle and a protocol | table, entity, domain type, mapper, repository, service, protocol, events and UI hooks |
ores.codegen.lookup_entity |
a coded, seeded reference table with no lifecycle | the table and its seed path, loaded in bulk |
ores.codegen.junction |
a many-to-many link between two entities | the join table, its struct, and the reads from both sides |
ores.codegen.family |
an entity whose parts must be written together, one owner | the family protocol: <family>_key, <family>_item, <family>_write and the four verb pairs |
ores.codegen.field_group |
a value shape reused by several entities, with no table of its own | a C++ header, composed into the entities that name it |
ores.codegen.operation |
messages that are not an entity's derived CRUD, such as a document or a domain operation | the protocol messages and the handler |
ores.codegen.module |
an index that groups a namespace's entity documents | no output of its own |
The compass scaffolder names each one: compass add entity_org,
lookup_entity, junction, field_group, and so on.
2.3. How are the entity's facts held
This is the group that goes wrong. The question is whether the entity is one table, several tables, or one table with nested C++ members.
| Pattern | Use when | Syntax | Generates |
|---|---|---|---|
| One table | the entity's facts change together and are read together | a plain entity |
one table, one type |
| Composite parent and parts | the entity's facts split by their own change reason, multiplicity or reader, and share one identity | a child FK with :bump_parent_version:, :list_by:, :list_by_uuid: and :list_by_as_of: |
the child tables, the parent version bump on any part write, the list-by-parent, and the as-of list |
| Associated entity | the other side has its own key and its own lifecycle | a second entity with a foreign key, no bump |
a table and a protocol of its own |
| Field group member | a value shape is nested in a C++ struct for a compile-time reason and has no table | domain_identity_group, domain_audit_group, or a ** Domain groups table |
nested C++ members |
The composite pattern is the target for an entity and its parts. A write to any part bumps the parent's version in the same transaction, so the parent's windows partition time into spans in which the parts are stable, and a read assembles the entity from the timeline with no link table. The mechanism is in Temporal Composite Entity Versioning: Target State and the target model in The atomised entity: a central row and its parts.
Do not use a field group member for an entity's facts. A field group nests the parts inside one struct, which is the aggregate shape Data-Oriented Design in ORE Studio rules out, and it puts the members on the wire as nested objects. The field-group patterns exist for a reused value shape and for the C1202 ceiling, not for storage.
Do not split a field set that fails the part test. A set with one row per entity, one change reason and one reader is a column group in a table's clothes. A part would buy a join, a message and a lookup for nothing.
2.4. What does the entity opt into
| Pattern | Use when | Syntax |
|---|---|---|
| Tenant and party isolation | every entity | :rls_tenant_isolation: and :rls_party_isolation: |
| Audit and versioning | every entity with a lifecycle | the audit columns and the version, generated by default |
| History | the entity's changes must be reviewable over time | the temporal columns and the versions sub-resource |
| Change events | another part of the system watches the entity | the event registrar annotation |
| Presentation | the entity has screens | Table display, Detail fields and the view_group cell |
| Custom repository methods | a read the derived set does not cover | the literate fragment mechanism |
| Insert-trigger validations | a rule the database must enforce on write | the Insert trigger section and its validation table |
| Variation | a group of related entity kinds states its variation | the profiles and features of the |
| Batched reads | an entity is read by a set of keys | nothing to declare: the repository derives read_latest over the primary key, and get_many carries it on the wire |
These are additions to a pattern, not patterns of their own. Each one is declared on the model and is never inferred from a column's presence, per Component Clean Standard and the NATS entity protocol specification.
2.5. What does the wire carry
| Pattern | Use when | Shape |
|---|---|---|
| Derived entity verbs | an ordinary entity | get and get_many, list and list_by_<relation>, put and put_many, delete and delete_many, all generated |
| Domain operation | a message that is not an entity's CRUD | declared messages and a handler |
| Composite family get and put | an entity whose facts are parts | the family's read and write, below |
| Read model | a screen whose rows span several entities | an entity derived from others, with list and get and no write |
A heterogeneous result set carries one typed array per concrete type, joined by key, per Polymorphic payloads over NATS. A screen whose rows span several entities is a read model, never a client join.
2.5.1. The generic put and the family put
Both exist, and they do different jobs. A composite entity has a derived protocol for its central row and a family protocol for the whole entity.
| Shape | Generic | Family |
|---|---|---|
| Get one | get |
get_<kind>_convention |
| Get many | get_many |
get_many_<kind>_conventions |
| Put one | put |
put_<kind>_convention |
| Put many | put_many |
put_many_<kind>_conventions |
- The generic
putwrites the central row alone. It is the derived verb of the central entity and it is what a rename, a market-data rebind or an audit-only change uses. - The family
putwrites the central row and that kind's parts in one transaction. It is a domain operation, because it spans several tables. Itsput_manyis one transaction for the whole batch, and the version precondition stays a property of each element. - The family
getmirrors the familyputfield for field.get_manyreturns the same shape for a set of keys.
A part that no family declares never appears in a family message. A kind is the set of parts its family protocol names.
A family put is one family. It is not a bundle: a bundle is several
related families applied in one unit_of_work, per
Data classifications.
2.6. Anti-patterns
- Nesting an entity's parts in one struct with a
** Domain groupstable ordomain_identity_group. Nesting is a wire and compile-time shape, not a storage shape. - Using a field group where a table is needed, so the data has no key and no row-level security.
- Composing flags that no facet consumes, which generates nothing and reads as if it did.
- Splitting a part with one row per entity, one change reason and one reader, which adds a join and a message for no access benefit.
- Reaching a part's column from a list filter or an order. The list reads the central row, so that is a read model.
3. See also
- Data classifications: entity, list, family, bundle, package and artefact — the closed set of classifications that comes before this page's choice.
- The data classification checklist — the form that records the classification and the pattern it selected.
- Codegen input org-file schema reference — the syntax for every pattern here, per model kind.
- Entity naming — the name each pattern gives the table, the type, the messages and the derived file, once the shape is chosen.
- ores.codegen — the generator itself, its facets and its addressing.
- How do I run codegen? — the operational recipe.
- Paste blocks: injecting custom code into generated files — how hand-written C++ survives regeneration.
- The atomised entity: a central row and its parts — the target model the composite pattern builds.
- Temporal Composite Entity Versioning: Target State — the bump and the window join behind the composite pattern.
- Data-Oriented Design in ORE Studio — the rule that rules out the nested aggregate.
- NATS entity protocol specification — the verb set, the read model and the as-of guarantee.
- Polymorphic payloads over NATS — one typed array per concrete type, joined by key.
- Component Clean Standard — the checks a component passes to be clean.
- Component Architecture Audit — the review that walks a component's models against these patterns.