Best Practices
Writing workflow
Section titled “Writing workflow”Follow this order when writing a new spec — each step builds on the previous:
1. Write template (optional) └─ Common stack, policies, conventions
2. Define system └─ Name, version, extends template
3. Add stack & intent └─ Technology choices, primary goal, outcomes, out_of_scope
4. Define interfaces └─ Contracts for inter-module communication
5. Create modules └─ owns, requires, implements, exports, api, artifacts
6. Define policies └─ Security, performance, quality rules with severity
7. Build pipeline └─ Steps with output types and gates
8. Validate └─ sodl validate spec.sodl
9. Compile └─ sodl compile spec.sodlModule design
Section titled “Module design”Single-responsibility
Section titled “Single-responsibility”One module should own one coherent domain. If owns has more than five items, consider splitting:
# ✅ Focused modulemodule PlayerModule: owns = ["Player entity", "Movement", "Shooting"] artifacts = ["src/player.rs"]
# ❌ Too broadmodule GameModule: owns = ["Everything"]Declare responsibilities with owns
Section titled “Declare responsibilities with owns”Even if a module has no implements, use owns to make its purpose explicit. This is what the AI reads to understand what the module is responsible for — and, crucially, what it is not responsible for.
Keep requires minimal
Section titled “Keep requires minimal”Every entry in requires is a coupling point. Prefer interface contracts over direct module-to-module dependencies:
# ✅ Depends on interface — swappablemodule OrderAPI: requires = [OrderRepository]
# ❌ Depends on concrete module — brittlemodule OrderAPI: requires = [PostgresOrderRepository]Templates and inheritance
Section titled “Templates and inheritance”Create a base template for each stack
Section titled “Create a base template for each stack”Encode stack-level conventions (auth, error handling, logging policies) in a template so every system that extends it inherits them automatically:
template "PythonAPIBase": stack: language = "Python 3.12" web = "FastAPI" policy Security: rule "Validate all input with Pydantic" severity=critical rule "No hardcoded secrets in code" severity=critical policy CodeQuality: rule "All public functions have docstrings" severity=medium
system "UserAPI" extends "PythonAPIBase": # Inherits Security and CodeQuality policies automatically stack: database = "PostgreSQL" auth = "JWT"Use inheritance operations for targeted changes
Section titled “Use inheritance operations for targeted changes”Instead of copying a base spec and editing it, use override, append, and remove to make surgical changes:
system "AdminAPI" extends "PythonAPIBase": # Change a single stack value override stack.auth = "OAuth2"
# Add a new policy rule without rewriting the policy append policy.Security.rules += "Admin endpoints require 2FA"
# Remove a rule that doesn't apply here remove policy.CodeQuality.rules -= "All public functions have docstrings"Policies
Section titled “Policies”At least one critical policy per project
Section titled “At least one critical policy per project”Every non-trivial system should have at least one critical-severity rule. These are the hard constraints the AI must never violate. Common candidates:
- No SQL injection
- Passwords must be hashed
- Auth required on all write endpoints
- No hardcoded secrets
Write verifiable rules — not adjectives
Section titled “Write verifiable rules — not adjectives”Rules are only useful if they can be checked. Avoid adjectives (fast, secure, clean). Use thresholds, patterns, and observable behaviors:
# ✅ Verifiablepolicy Performance: rule "Response time < 200ms at p95" severity=high rule "Memory usage stays under 500MB" severity=high rule "Particle count limited to 500 maximum" severity=medium
# ❌ Not verifiablepolicy Performance: rule "Make it fast" severity=high rule "Use less memory" severity=mediumUse rationale fields to explain decisions
Section titled “Use rationale fields to explain decisions”When a rule needs context, document the “why” so the AI (and future developers) understand the intent:
policy Security: rule "Passwords hashed with bcrypt (cost factor 12)" severity=criticalSeverity guide
Section titled “Severity guide”| Level | Use when | AI behavior |
|---|---|---|
critical | Data loss, security breach, core invariant | Must not violate |
high | Required by the system design | Must follow |
medium | Industry best practice | Should follow |
low | Nice-to-have optimization | May follow |
Interfaces
Section titled “Interfaces”Define interfaces before modules
Section titled “Define interfaces before modules”Design contracts first, implement second. This forces you to think about the boundary before the internals:
# ✅ Contract firstinterface OrderRepository: method create(order: OrderCreate) -> Order method get_by_id(id: UUID) -> Optional[Order] method update_status(id: UUID, status: OrderStatus) -> Order
# Then implementmodule PostgresOrderRepository: implements = [OrderRepository] exports = [OrderRepository]Use invariants in interfaces to document guarantees
Section titled “Use invariants in interfaces to document guarantees”Interface invariants are constraints that every implementation must satisfy — they belong in the interface, not scattered across module implementations:
interface EnemyRepository: method spawn(type: str) -> Enemy method get_all() -> List[Enemy] invariants: invariant "Enemy IDs are unique" invariant "Spawned enemies are immediately queryable"Resolve circular dependencies through interfaces
Section titled “Resolve circular dependencies through interfaces”If two modules need to call each other, introduce a shared interface to break the cycle:
# ❌ Circular (compiler will reject)module A: requires = [B]module B: requires = [A]
# ✅ Resolved with interfaceinterface AInterface: method do_a() -> Result
module A: implements = [AInterface] requires = [BInterface]
module B: implements = [BInterface] requires = [AInterface]Acceptance tests
Section titled “Acceptance tests”Every module needs measurable acceptance criteria
Section titled “Every module needs measurable acceptance criteria”Acceptance tests in the acceptance block define the Definition of Done. They are evaluated when the pipeline step’s gate is checked:
module AuthAPI: acceptance: test "registers new user and returns 201" test "rejects duplicate email with 409" test "returns JWT on valid login" test "returns 401 for invalid credentials" test "rate limits to 5 requests per minute per IP"Good test criteria
Section titled “Good test criteria”- Observable behavior, not implementation detail: “returns 401” not “checks the token variable”
- Both positive and negative cases: success paths and error paths
- Measurable thresholds where applicable: “in < 30 seconds”, “at 100 concurrent users”
- Present tense, active voice: “registers new user” not “new user should be registered”
Anti-pattern: vague acceptance criteria
Section titled “Anti-pattern: vague acceptance criteria”# ❌ Not testableacceptance: test "authentication works correctly" test "handles errors"
# ✅ Testableacceptance: test "returns JWT with 1-hour expiry on valid login" test "returns 401 with error message on invalid password" test "returns 429 after 5 failed attempts in 60 seconds"Pipelines
Section titled “Pipelines”Keep steps atomic
Section titled “Keep steps atomic”Each step should have one output type and one verifiable gate. If you find yourself listing many unrelated things in one step, split it:
# ✅ Atomic stepspipeline "Development": step Design: output = design require = "Define architecture and data models" step ImplementCore: modules = ["AuthModule", "UserModule"] output = code gate = "Unit tests pass" step ImplementFeatures: modules = ["ProductModule", "OrderModule"] output = code gate = "Feature tests pass" step Test: output = tests gate = "Coverage >= 85%"
# ❌ Monolithicpipeline "Development": step DoEverything: output = codeGates are pass/fail contracts
Section titled “Gates are pass/fail contracts”Write gate values as clear pass/fail criteria, not process descriptions:
# ✅ Pass/fail criteriagate = "All unit tests pass"gate = "Lighthouse score > 90"gate = "Process 10k events/sec without lag"
# ❌ Process description (not a gate)gate = "Run the tests"Match artifacts to real directory structure
Section titled “Match artifacts to real directory structure”Artifact paths should reflect where the generated files will actually live. Inconsistencies cause the AI to create files in unexpected locations:
module AuthModule: artifacts = ["app/api/auth.py", "tests/test_auth.py"]
module UserModule: artifacts = ["app/api/users.py", "tests/test_users.py"]Pre-compile checklist
Section titled “Pre-compile checklist”Before running sodl compile, verify:
□ Syntax: colons after declarations, double quotes around names□ All interfaces defined before they appear in requires/implements□ Every requires resolves to an exports or implements□ No circular dependencies between modules□ Pipeline steps in logical order□ Severity levels: critical/high/medium/low (no other values)□ Acceptance tests are measurable and observable□ Artifact paths are relative to project root□ out_of_scope clearly defined to prevent AI scope creep□ Policies are specific and testable (no vague adjectives)□ All interface methods will be implemented by a module□ Module names are unique within the systemCommon errors
Section titled “Common errors”| Error | Problem | Fix |
|---|---|---|
system "App" (no colon) | Missing block opener | system "App": |
system MyApp: | Unquoted name | system "MyApp": |
method get(id: string) | Wrong type name | Use str, not string |
severity=urgent | Invalid severity | Use critical, high, medium, low |
requires = [Unknown] | Undefined interface | Define interface before use |
step Build: (no output) | Missing output type | Add output = code |
| Mixed tabs/spaces | Parser error | Use 2 spaces throughout |
| Duplicate module names | Name collision | Use unique names |