F5 · the engine · module 2
Modeling the Portal domain
With the Portal running behind a thin server, the engine needs a shape before it needs behavior. That shape is the domain model, and it is built in three layers. Each entity — a user, a lesson, an enrollment — is a plain struct with a typespec. Entities are grouped into bounded contexts — Accounts, Catalog, Learning — modules that own their data and reference one another only by branded id. And each context exposes a small public API that validates input and keeps its internals private. No Ecto, no database rows yet; the model is data and the functions that guard it.
A context, seen from the inside out: structs at the core, a boundary around them, a public API on top. Select a layer to see what it is and which dive builds it.
Reference by id, not by struct
user: %User{id: usr_7f3, …}
The whole struct is embedded — the contexts are coupled.
Anatomy of a context
Take the Learning context as the example. Inside it are the structs it owns; the module itself is the boundary that holds them; and the handful of public functions on its surface are the only way another part of the system may touch it. Build the model in that order — data, boundary, interface — and each later module has an obvious home for every rule.
A context, inside out · select a layer
layer: Structs
built in: F5.02.1
The domain is plain data with a guarded edge. Get the structs, the boundary, and the API right and the engine has somewhere to put every command, query, and rule that follows.
Three deep dives
In order: the structs and typespecs that give an entity its shape; the bounded contexts that group them and keep them decoupled; and the small public API that is the only way into a context.
@enforce_keys for the required fields, defaults for the rest, and a @type t the compiler and Dialyzer can check.{:ok, struct} or {:error, reason} — the only way in.F5.01 · it runs
A thin server puts the Portal on a port, but its handlers call an engine that has no shape yet.
F5.02 · give it a shape
Model the entities as structs, group them into contexts, and expose each context through a small API.
Start with structs & typespecs, then bounded contexts, then a context’s public API. The next module, F5.03 — Tracer bullets, drives one use case end to end through this model. For the map of the domain, see the design brief: the domain model.
References
Modeling a domain in structs and bounded contexts.
Sources
- Elixir — Structs — entities with enforced keys.
- Elixir — Typespecs —
@typeand@spec. - Phoenix — Contexts — bounded contexts and their public APIs.
- Martin Fowler — Bounded Context — the boundary as a unit of decoupling.
Related in this course
- F5.02.1 · Structs & typespecs
- F5.02.2 · Bounded contexts
- F5.02.3 · A context’s public API
- F5.0.2 · The domain model — the map of all three contexts.
- F4.07 · Identifiers, Snowflake & branded ids — the ids that key every entity.