jonnifyelixir
F5 · Pragmatic Programming / F5.02 · domain

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

ACCOUNTS · CONTEXT %User{id: usr_7f3, name: …} CONTEXT BOUNDARY LEARNING · CONTEXT %Enrollment{…} user: %User{id: usr_7f3, …} embeds the whole struct · hard link a reference crosses the boundary

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

PORTAL.LEARNING · CONTEXT PUBLIC API enroll/2 · record_progress/2 · courses_of/1 ENTITIES · STRUCTS %Enrollment{} %Progress{} data at the core · a boundary around it · a public API on top

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.

F5.02.1
Structs & typespecs
@enforce_keys for the required fields, defaults for the rest, and a @type t the compiler and Dialyzer can check.
F5.02.2
Bounded contexts
Accounts, Catalog, Learning — each owns its entities and references the others only by branded id, so each can change alone.
F5.02.3
A context's public API
A smart constructor that validates and returns {: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

Related in this course