Skip to content
Text size
Text size

Model the task domain

A Nomos domain begins with business language, not tables or endpoints.

Our first nouns are deliberately unsurprising:

export const TodoList = aggregate("TodoList", {
title: t.string().merge(Lww),
owner: attributionField(t.string().merge(Lww)),
}).public();
export const Todo = aggregate("Todo", {
listId: t.ref(TodoList),
text: t.string().merge(Lww),
done: t.bool(),
tags: t.set(t.string()).merge(AddWins),
}).public();

This tiny model already says more than a conventional schema:

  • a task belongs to one continuing list identity;
  • text has an explicit concurrent-edit rule;
  • concurrent tag additions are retained rather than accidentally overwriting one another;
  • ownership is attributed from the verified participant rather than trusted from a form field.

Business changes are named verbs:

export const addTodo = directive("addTodo")
.creates(Todo)
.payload(z.object({
listId: idOf(TodoList),
text: z.string().min(1),
}))
.plan((input) => {
create(Todo)
.set("listId", input.listId)
.set("text", input.text)
.set("done", false);
return [];
});
export const toggleTodo = directive("toggleTodo")
.mutates(Todo)
.payload(z.object({ todoId: idOf(Todo), done: z.boolean() }))
.plan(({ todoId, done }) => [
set(instance(Todo, todoId), "done", done),
]);

The generated application exposes addTodo and toggleTodo. The UI does not learn how those verbs are stored, synchronized or replayed.

Reads are part of the domain too:

export const todosByList = query("todosByList")
.key("listId")
.returns(Todo);
export const openTodosByList = count("openTodosByList")
.of(Todo)
.where((todo) => todo.field("done").eq(false))
.by("listId");

That gives the product a reactive list and an incrementally maintained open-task count without creating a second hand-written read model.

Grow sophistication without losing the plot

Section titled “Grow sophistication without losing the plot”

The same domain can then add assignees, comments, priorities, due dates, workflow states, dependencies, saved views and notifications. Each addition should answer a product question a task-app user already understands.

That familiarity is the teaching advantage: when we introduce relationship-based sharing, per-item history, offline convergence or schema evolution, engineers can judge the trade-off immediately. They are not simultaneously decoding an unfamiliar industry.

Next, use the generated application to turn this law into responsive UI.