Skip to content

Best Practices

Run dovetail fmt and let it choose layout. Use camelCase identifiers, descriptive names, and .dove filenames. A file containing one type’s module can use that type’s name, such as Order.dove; other source filenames use camelCase. Name new factory functions make, or parse when construction validates external input. Existing libraries may use different names; call the API they actually expose.

Keep examples and application code explicit about their dependencies. Import named extensions before using their methods, including within the same package.

Use records and enums for immutable domain data. Represent mutually exclusive states with enum variants carrying the fields each state requires. Use private newtypes when values must be validated before use. Private records reserve construction and with updates for their associated module.

Record fields are immutable bindings, not a deep freeze: a record holding an array or a mutable class still shares that object’s state. Prefer lists or other immutable contents when the model requires immutable snapshots.

Use classes for owned mutable state. Keep mutation behind a small API. A type’s shape and its behavior should make valid transitions easy to identify.

Use Option for expected absence, and Result for an operation that may fail. In async code, keep failures in the Async<T, E> error channel. Handle an outcome, return it, or compose it into more work. An exhaustive match forces new outcomes to be considered when the model changes.

Warnings catch discarded Result, Async, and Resource expressions. Use let _ = expression only when deliberately ignoring that value. It neither awaits an async computation nor acquires a resource. Assigning to a named variable is not proof that the variable will later be used; these warnings are not a general unused-variable analysis.

Reserve panic, assert, and .require for bugs, tests, and established invariants. A synchronous panic terminates execution; async code can represent a panic in its failure cause and run scoped cleanup. Panics are not the normal input-validation API.

Use use for external resources. Keep acquisition and release bounded, and choose fork versus forkBackground according to whether a child should finish normally or be cancelled when its scope ends. Read Resources and Async Programming before building long-lived services.

Start with one project. Split packages or projects when they represent a real boundary, and keep dependencies acyclic. Tests next to a rule make it easy to change them together; integration tests belong in test/.

Keep I/O at the edges of business rules. Pass facts, clocks, and generated IDs as values. Give persistence and transport adapters interfaces owned by their consumers. For larger applications, see Project Structure and The Domain Layer.

Check in the manifest and dependency lockfile. Format, check, and test in CI using a matching compiler version. Start from Tool Commands for the command sequence.