Authoring Features
The mental model
A feature is a user-facing capability — something a person can do with the system that delivers value. It answers the question: “What can the user accomplish, and how do we know it works?”
Features sit between the abstract (domains) and the concrete (flows, surfaces). They declare what the system must do without specifying how it is built.
Requirements: behaviour, not implementation
Requirements describe observable behaviour from the user’s perspective. They should be verifiable without knowing the implementation.
Good requirement: “The system must allow a user to save any valid URL as a bookmark.” Bad requirement: “The system must insert a row into the bookmarks table with a non-null URL column.”
Use MoSCoW priorities (must, should, could, wont) to communicate trade-off decisions. A feature with only must requirements has no negotiation room — be honest about what is truly non-negotiable.
The API section
A feature’s API declares the actions and events it exposes to the rest of the system. This is the feature’s public contract.
Key distinction: a feature’s API often delegates to a domain’s API. If domain/auth declares an operation called login, the feature might expose an action called submit-credentials that orchestrates the login flow (validation, UI feedback, redirect). The feature API is user-journey-scoped; the domain API is business-logic-scoped.
Relationship to domains
Features use domains — they do not redefine them. Declare the dependency with the domains attribute:
{% feature id="authentication" domains="domain/auth" %}This tells the compiler (and readers) that this feature depends on the auth bounded context. If you find yourself writing a glossary inside a feature, that vocabulary probably belongs in a domain.
Acceptance criteria
Criteria live in a {% criteria %} block outside the feature tag. They follow the Given/When/Then pattern and must be:
- Specific — reference concrete states and outcomes
- Testable — an engineer should be able to write an automated test from the criterion alone
- Traceable — each criterion references its parent requirement via
requirement="..."
Cover both the happy path and meaningful failure modes. If a requirement has no failure criterion, ask yourself whether the requirement is complete.
Deciding what is “one feature”
A feature should be cohesive — all its requirements serve a single user goal. If you can split it into two capabilities a user might want independently, it is probably two features.
Conversely, do not create a feature for every button. The test: “Could I ship this feature alone and a user would notice value?”
Common mistakes
| Mistake | Why it hurts |
|---|---|
| Conflating features with domains | A feature is not a bounded context. If your feature has a glossary and a DBML model, you are writing a domain. |
| Requirement sprawl | Stuffing 20 requirements into one feature means it is too large. Split by user goal. |
| Vague criteria | ”The system should work correctly” is not testable. Every criterion needs observable inputs and outputs. |
| Implementation leaking in | ”Use Redis for caching” is a blueprint concern, not a feature requirement. |
| Missing roles | If no roles attribute is set, the feature has no clear actor. Every capability is for someone. |
| Duplicating domain API | If you copy-paste actions from the domain, you now have two sources of truth. Reference, do not repeat. |
Evolution
Features grow through refinement: new requirements surface during development, criteria get added as edge cases appear. When a feature becomes too large, extract a sub-feature and link them through shared domain references. Use the scope attribute to control which requirements are visible to agents vs. the public documentation site.