jonnifyelixir
Contents / F5 · Pragmatic Programming / The domain model

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

ACCOUNTS User USR Session SES CATALOG Course CRS Lesson LSN Page PGE LEARNING Enrollment ENR Progress PRG

      

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

Related in this course