F5 · system design · 2 of 3
The domain model
The core holds the Portal’s data, and that data has a shape worth fixing before any code. The engine owns three bounded contexts — Accounts, Catalog, and Learning — and each context owns a few entities, modeled as plain Elixir structs and keyed by the branded Snowflake ids from F4. A context is a seam: it owns its entities, guards its own rules, and exposes a small public API, so the rest of the engine depends on the API rather than the data. This is the model F5.02 builds.
Select a context to see the entities it owns and the id namespaces that key them. The same branded id convention from F4 is the pivot key across every context.
Three contexts
Contexts keep the model honest: a learner’s Enrollment lives in Learning and
only references a User and a Course by id, so Accounts and
Catalog can change their internals without breaking it. Every entity carries a branded id whose three-letter
namespace says which kind it is — the cross-context pivot key.
The contexts · select one
context: Accounts
id namespaces: USR, SES
A context is the unit of decoupling in the engine. Draw the boundaries here, on the data, and the modules that follow have an obvious home for every rule.
An entity in code
An entity is a struct with a typespec and enforced keys: the id and the foreign keys are branded ids, so an
Enrollment is unambiguous about which user and which course it joins, and the
namespaces make a malformed reference visible at a glance. There is no Ecto and no database row here —
the struct is the model; how it is stored is the store’s problem (F4), not the domain’s.
# a Learning entity — a plain struct keyed by branded ids (F4 convention) defmodule Portal.Learning.Enrollment do @enforce_keys [:id, :user_id, :course_id] defstruct [:id, :user_id, :course_id, enrolled_at: nil, progress: 0] @type t :: %__MODULE__{ id: String.t(), # "ENR0Nb..." user_id: String.t(), # "USR0Nb..." -> Accounts course_id: String.t(), # "CRS0Nb..." -> Catalog progress: non_neg_integer() } end
a bounded context
Accounts, Catalog, Learning — each owns its entities and a small API.
structs keyed by branded ids
References cross contexts by id; the namespace says which kind.
Next in the design brief: the command & event flow — how these entities change over time. Back to the blueprint.
References
Modeling a domain in structs and bounded contexts.
Sources
- Phoenix — Contexts — bounded contexts and their public APIs.
- Elixir — Structs — modeling entities with enforced keys.
- Elixir — Typespecs —
@typefor each entity.
Related in this course
- F5.0.1 · The Portal engine blueprint — where the model sits.
- F5.0.3 · The command & event flow — how entities change.
- F4.07 · Identifiers, Snowflake & branded ids — the ids that key every entity.
- F5 · Pragmatic Programming