domain-modeling
Skill0 starsChanged 50 days agoMITView on GitHub
Deep dive on designing a domain entity in packages/domain — Equatable value equality, the hand-written copyWith sentinel pattern for nullable fields, variant inheritance for entities tied to another entity, the sortedX getter convention, and where business-rule methods belong; use whenever designing or reviewing an entity or value object.
195 other projects use a version of this.
Adopted by
Most recently taken up by
- anorth/agent-skillstheir own version, first seen Nov 8, 2026
- chrysogonus/power-agentstheir own version, first seen Nov 8, 2026
- The-Vibe-Company/quivrtheir own version, first seen Nov 8, 2026
- AJschoons/robowrighttheir own version, first seen Nov 8, 2026
- tornikegomareli/Skillstheir own version, first seen Nov 8, 2026
- miklschmidt/archboardtheir own version, first seen Nov 8, 2026
Used with
Projects with it also use setup-ts-deep-modules, decision-mapping and 10 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
liufeng1976Skillsetup-ts-deep-modules
Wire dependency-cruiser into a TypeScript repo so each package is a deep module — implementation hidden in subfolders, reachable only through its entry-point files. User-invoked.
Also in 43 projects that use this
FabienDehopreSkilldecision-mapping
Turn a loose idea into a sequenced map of investigation tickets, then drive them to resolution one at a time.
Also in 8 projects that use this
LesaniSkillwriting-beats
Shape an article as a journey of beats, choose-your-own-adventure style. The user picks a starting beat from the raw material, you write only that beat, then offer options for where to pivot next, beat by beat, until the article reaches a natural end. Use when the user has raw material and wants to assemble it as a narrative rather than an argument.
Also in 62 projects that use this
RoentekSkillloop-me
Grill me about specs for the workflows I want to build, within this workspace.
Also in 56 projects that use this
AWSSkillwizard
Generate an interactive bash wizard that walks a human through steps only they can perform. Use when provisioning infrastructure, setting up credentials or CI secrets, walking an unfamiliar third-party dashboard, or running a one-off migration or cutover. Don't invoke this for steps the agent can perform itself.
Also in 134 projects that use this
LesaniSkillwriting-shape
Take a markdown file of raw material and shape it into an article through a conversational session — drafting candidate openings, growing the piece paragraph by paragraph, arguing about format (lists, tables, callouts, quotes) at each step. Use when the user has a pile of notes, fragments, or a rough draft and wants help turning it into something publishable.
Also in 60 projects that use this
- +1 more from Lesani
Other versions195 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 195
- mattpocock/skills★ 309ksame name
- QuivrHQ/quivr★ 40ksame name
- The-Vibe-Company/quivr★ 40ksame name
- windmill-labs/windmill★ 18ksame name
- GreyDGL/PentestGPT★ 16ksame name
- formbricks/formbricks★ 13ksame name
- ArcReel/ArcReel★ 6.3ksame name
- vinvcn/mattpocock-skills-zh-CN★ 5.5ksame name
- withcoral/coral★ 4.9ksame name
- bestofjs/bestofjs★ 3.1ksame name
185 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository35 other files it gives its agents.
- AGENTS.mdAGENTS.md
- bootstrap-projectSkill
- choose-data-stackSkill
- code-reviewSkill
- conventional-commitsSkill
- data-providerSkill
- error-handlingSkill
- flavors-and-flagsSkill
- flutter-fvmSkill
- localizationSkill
- new-featureSkill
- quality-gateSkill
- routingSkill
- state-managementSkill
- testingSkill
- themingSkill
- ui-screenSkill
- bootstrap-projectSkill
- choose-data-stackSkill
- code-reviewSkill
- conventional-commitsSkill
- data-providerSkill
- domain-modelingSkill
- error-handlingSkill
- flavors-and-flagsSkill
- flutter-fvmSkill
- flutter_fvmSkill
- localizationSkill
- new-featureSkill
- quality-gateSkill
- routingSkill
- state-managementSkill
- testingSkill
- themingSkill
- ui-screenSkill
The file
We split this skill into the rules it gives an agent
9 rules. 0 are common, 2 are distinctive, and 7 are very specific to this project.
0
Common
Most of the 183 edited versions we read keep these.
2
Distinctive
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 14
Every example below models a generic Task/Project domain.
- Very specific: line 46
Every constructor field is final.
- Very specific: line 105
Do not apply this pattern to a non-nullable field (title, isDone above) — a plain T?
- Distinctive: line 115
When an entity is *sometimes* tied to another entity, resist adding a nullable foreign-key field to the base class:
- Very specific: line 180
Never assume a List field is already in a meaningful order — a data provider may return rows in insertion order, primary-key order, or …
- Very specific: line 253
If the rule needs only this's fields (plus maybe a passed-in "current time"/"current user" for testability, as now is above), it's an …
- Distinctive: line 256
If the rule needs to *combine data from two different entities* that aren't in a parent/child relationship (e.g.
- Very specific: line 261
If the rule needs BuildContext, a DateTime.now() with no way to inject a fixed clock for a test, or any Flutter/UI concept, it does not …
- Very specific: line 265
Never write the same conditional (dueDate != null && !isDone && ...) inline in more than one Cubit or widget — that duplication is the …
“Common” is measured: kept by at least 6 in 10 of the 183 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Domain modeling: designing an entity
An entity lives in packages/domain/lib/src/entities/. It is pure Dart —
no flutter, no data, no storage/network package (packages/domain/AGENTS.md
and AGENTS.md section 2) — and it is the one place business rules that
belong to the thing itself (not to a particular screen or use case) should
live. This skill covers entity design in isolation; for the full
entity-to-screen sequence see .agents/skills/new-feature/SKILL.md.
Every example below models a generic Task/Project domain. Do not copy
naming from an unrelated app's domain model — mirror the shape
demonstrated here and in
packages/domain/lib/src/entities/feature_flag.dart, not any specific
example's field names.
1. Base shape: Equatable, immutable, no generated code
import 'package:equatable/equatable.dart';
final class Task extends Equatable {
const Task({
required this.id,
required this.title,
required this.isDone,
this.dueDate,
});
final String id;
final String title;
final bool isDone;
final DateTime? dueDate;
@override
List<Object?> get props => [id, title, isDone, dueDate];
}final class, notclass— closes the entity to further subclassing from outsidepackages/domainunless you're deliberately modeling a variant (section 3 below), in which case the base becomes non-finalon purpose.- Every constructor field is
final; there are no setters. A "change" is always a new instance viacopyWith. propslists every field that participates in equality — forgetting one here is the most common way two logically-different entities compare as equal in a test or aBlocBuilderrebuild check.- No
freezed, nojson_serializable.copyWithis hand-written (seeentities.dart's doc comment) so the sentinel pattern below is expressible without a code-generation dependencydomainisn't allowed to have.
2. copyWith and the _unset sentinel for explicitly-nullable fields
A naive copyWith cannot distinguish "leave this field alone" from
"explicitly set it to null" when the parameter itself is nullable — both
cases look like passing null. The fix is a private sentinel value that is
never a legitimate argument:
const _unset = Object();
final class Task extends Equatable {
const Task({
required this.id,
required this.title,
required this.isDone,
this.dueDate,
});
final String id;
final String title;
final bool isDone;
final DateTime? dueDate;
Task copyWith({String? title, bool? isDone, Object? dueDate = _unset}) => Task(
id: id,
title: title ?? this.title,
isDone: isDone ?? this.isDone,
dueDate: identical(dueDate, _unset) ? this.dueDate : dueDate as DateTime?,
);
@override
List<Object?> get props => [id, title, isDone, dueDate];
}Why each part matters:
dueDate's parameter type isObject?, notDateTime?— this is what lets it accept the sentinel (_unset, itself anObject) as its default value. ADateTime?parameter could not default to anObject.- The check is
identical(dueDate, _unset), notdueDate == _unset—identicalis the correct equality for a marker object;==would work here too sinceObjectdoesn't override it, butidenticaldocuments the intent unambiguously. - Three call sites, three distinct outcomes:
task.copyWith(); // dueDate unchanged task.copyWith(dueDate: DateTime(2026)); // dueDate set to 2026-01-01 task.copyWith(dueDate: null); // dueDate explicitly cleared - Do not apply this pattern to a non-nullable field (
title,isDoneabove) — a plainT?parameter with?? this.fieldis sufficient there, and adding the sentinel unconditionally to every field is needless ceremony that obscures which fields actually need it. idis deliberately not acopyWithparameter at all — identity doesn't change across acopyWithcall; if code needs a different id, it should construct a newTask(...), notcopyWithone.
3. Variant inheritance: a non-nullable reference on a subclass, not a nullable field on the base
When an entity is sometimes tied to another entity, resist adding a nullable foreign-key field to the base class:
// AVOID — every caller has to null-check projectId even when the domain
// rule is "an assigned task always has a project".
final class Task extends Equatable {
const Task({required this.id, required this.title, this.projectId});
final String? projectId;
}Model it as a subclass with the reference made non-nullable instead:
class Task extends Equatable {
const Task({required this.id, required this.title, required this.isDone});
final String id;
final String title;
final bool isDone;
@override
List<Object?> get props => [id, title, isDone];
}
/// A [Task] that has been assigned to a project. [projectId] is guaranteed
/// non-null — there is no such thing as an [AssignedTask] without one.
final class AssignedTask extends Task {
const AssignedTask({
required super.id,
required super.title,
required super.isDone,
required this.projectId,
});
final String projectId;
@override
List<Object?> get props => [...super.props, projectId];
}The base class (Task here) must be non-final (or sealed/abstract)
for this to compile — only make it non-final when you actually intend a
subclass; a plain value entity with no variant stays final class as in
section 1.
Callers narrow with a type check instead of a null check:
// Before (nullable field): if (task.projectId != null) { ... task.projectId! ... }
// After (variant):
if (task is AssignedTask) {
scheduleReminder(task.projectId); // no `!`, no nullability to forget
}This scales to more than one variant dimension — e.g. RecurringTask extends
Task with a non-nullable frequency — each modeling one "this kind of task
always has this extra, mandatory piece of data" rule as a type, not a
null-checked optional field shared by every kind.
4. The sortedX getter convention
Never assume a List field is already in a meaningful order — a data
provider may return rows in insertion order, primary-key order, or whatever
order its underlying query happened to produce. If a field's order matters
to a caller, expose a getter that sorts on demand instead of promising (or
silently relying on) the backing field being pre-sorted:
final class Project extends Equatable {
const Project({required this.id, required this.name, required this.tasks});
final String id;
final String name;
final List<Task> tasks;
/// Tasks in due-date order, undated tasks last. Use this instead of
/// [tasks] directly whenever display or processing order matters — the
/// backing field's order is not guaranteed.
List<Task> get sortedTasks {
final sorted = [...tasks];
sorted.sort((a, b) {
if (a.dueDate == null && b.dueDate == null) return 0;
if (a.dueDate == null) return 1;
if (b.dueDate == null) return -1;
return a.dueDate!.compareTo(b.dueDate!);
});
return sorted;
}
@override
List<Object?> get props => [id, name, tasks];
}props still lists the unsorted tasks — a getter is derived, not stored
state, so it must not appear in props (it would just recompute the same
comparison Equatable already does structurally on the list).
5. Where business-rule methods belong
A rule that only needs the entity's own fields to evaluate belongs on the entity as a method or getter — not duplicated inside every Cubit that needs the answer, and not left for the UI layer to compute from raw fields:
final class Task extends Equatable {
const Task({
required this.id,
required this.title,
required this.isDone,
this.dueDate,
});
final String id;
final String title;
final bool isDone;
final DateTime? dueDate;
/// True once [dueDate] has passed and the task is still open. A screen
/// renders this as an overdue badge; a Cubit filters on it to build an
/// "overdue" list — both read this getter instead of re-deriving the
/// `dueDate.isBefore(now) && !isDone` check themselves.
bool isOverdue({DateTime? now}) {
final reference = now ?? DateTime.now();
return dueDate != null && !isDone && dueDate!.isBefore(reference);
}
@override
List<Object?> get props => [id, title, isDone, dueDate];
}Guidelines for the boundary:
- If the rule needs only
this's fields (plus maybe a passed-in "current time"/"current user" for testability, asnowis above), it's an entity method. - If the rule needs to combine data from two different entities that
aren't in a parent/child relationship (e.g. "is this task visible to this
user"), it belongs on the repository instead
(
packages/domain/lib/src/repositories/) — repositories hold the orchestration a use case would hold elsewhere (packages/domain/AGENTS.md). - If the rule needs
BuildContext, aDateTime.now()with no way to inject a fixed clock for a test, or any Flutter/UI concept, it does not belong on the entity — keep the entity method pure and deterministic and let the Cubit or widget supply the "now"/"context" it evaluates against. - Never write the same conditional (
dueDate != null && !isDone && ...) inline in more than one Cubit or widget — that duplication is the signal the rule was supposed to live on the entity.
Checklist
- Entity
extends Equatable;propslists every field, including ones added later. copyWithis hand-written; the sentinel pattern (_unset) is used only for fields that must be explicitly clearable tonull— not applied to non-nullable fields.- A field tied to another entity that is sometimes absent is modeled as a subclass with a non-nullable reference, not a nullable field on the base.
- A
Listfield whose order matters has asortedXgetter; the backing field itself is never assumed sorted. - A business rule expressible from the entity's own fields is a method on the entity, not duplicated across Cubits.
- No
flutter,dart:io, or storage/network import —tool/check_architecture.dartrejects it (AGENTS.md section 2). - Exported from
entities.dart, andentities.dartis exported frompackages/domain/lib/domain.dart. - A test exists under
packages/domain/test/entities/coveringcopyWith's clear-vs-leave-alone behavior for every nullable field and any entity method added (see AGENTS.md section 6 and.agents/skills/testing/SKILL.md).
Common mistakes
- Forgetting a field in
props— two entities that differ only in that field compare as equal, which silently breaksBlocBuilder's rebuild check and any test asserting inequality. - Giving every nullable field the
_unsetsentinel treatment "for consistency" even when nothing ever needs to clear it back tonull— adds a parameter type that isn'tT?for no behavioral benefit. - Adding a nullable foreign-key field to a base entity instead of a variant
subclass, then scattering
if (x.fooId != null)null-checks across Cubits and widgets instead of a singleis AssignedTaskcheck. - Sorting a list field in its constructor and calling that "the sortedX convention" — the getter must exist and do the sort, precisely because the backing field's order is not a guarantee any caller should rely on.
- Putting a rule that needs
BuildContext,context.l10n, or an un-injectableDateTime.now()directly on the entity, making it impossible to unit test withpackage:testalone (packages/domain/AGENTS.md→ "Testing": purepackage:test, no Flutter dependency).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

