Andromeda's layers stay apart because tests say so, not because people remember to keep them apart. Each crate declares which crates it may depend on. A set of source checks fails the build when SQL, untyped JSON, a second reading of a rule or a stray dependency turns up where it doesn't belong. For you, the result is that one rule has one implementation, and an endpoint can't quietly judge a request differently from its neighbour.
Dependency contracts
Almost every workspace crate carries a contract, a test generated by a macro that lists the workspace crates and the external crates it is allowed to name. The few that have none are build tooling. This one belongs to the name dictionary crate:
andromeda_architecture_checks::dependency_boundary! {
allow_workspace_dependencies: [
"andromeda-configuration",
"andromeda-ngsi-ld-lexicon",
"andromeda-persistence-failure",
"andromeda-postgresql-pool",
"andromeda-postgresql-query",
"andromeda-postgresql-schema",
"andromeda-postgresql-seaquery",
],
...
}
The test reads the crate's own Cargo.toml and fails when its dependencies don't match the list. Adding a dependency therefore means editing the contract, where a reviewer sees the new edge. An edge that would let a domain crate reach the database, or the database layer reach HTTP, doesn't get through unnoticed.
Source checks
Beside the contracts, a crate of checks reads the workspace's Rust source as a syntax tree, the way the compiler sees it, and fails on shapes that must not appear.
-
SQL outside the database adapter
One crate spells SQL and hands statement text to the driver. Every other crate holds statement values only, so no query is hand-written into a domain or HTTP crate. A second check keeps SQL out of the crates that model the specification.
-
General JSON trees
Production code may not build, hold or convert one. The check runs at zero, with no exceptions recorded. Typed model explains why.
-
Errors without types
An error must be a typed enum. A free-text error or a type-erased one fails. So does a client-facing message that breaks the wording rules of the error model.
-
Row-decoding failures turned into defaults
A column that can't be read is an error that propagates. Reading it as zero or as absent would answer you as if nothing were wrong.
-
A second reading of a rule
Date-times, the Scope grammar, linked entity retrieval and registration matching each have one owner, and code that reimplements one fails.
-
The entity codec in the database layer
PostgreSQL holds entity state as typed rows. A database crate that turns state into a JSON document fails.
-
Unvalidated constants
IRI and media-type constants must be validated values.
-
Files and modules that grow into dumping grounds
A production file past 600 lines fails, and one past the signal line of about 400 is tolerated only on a baseline that can only shrink. The baseline is empty today. Module names that describe no concept, such as utils, fail too.
-
Manifest hygiene
Every external dependency pins a full version, dependency tables are sorted, and external crates are declared in the crate that uses them.
The checks run as ordinary tests.
Lints
The compiler and Clippy hold every crate to the same lint levels, and a warning fails the build. No crate sets levels of its own, so a rule never holds in one crate and lapses in the next.
-
No panics in production code
unwrap, expect, panic!, todo!, unimplemented! and unreachable! are refused, and so is a driver method that panics on a missing or undecodable column. A failure becomes a typed error that propagates, so a bad request or a broken row produces an error response, never a crashed worker. Tests are exempt. A panic is how a test fails, and an expect message reads as the assertion.
-
No silent numeric conversions
An as cast that could truncate, wrap or lose a sign is refused. A conversion that can fail goes through TryFrom and handles the failure, so a count or an offset read from a client or a row never turns into a different number.
-
Concurrency mistakes caught at compile time
A lock guard held across an .await, an async fn whose future is not Send, and an Arc around a type that cannot cross threads are refused. Each would either deadlock the runtime or keep a task from moving between worker threads.
-
Every match decides every variant
A wildcard arm over an enum is refused. Adding a variant then fails the build at every match that has to decide what it means, instead of letting it fall into a default nobody chose.
-
Exceptions are explained and expire
A lint can only be suppressed with #[expect] and a written reason, never #[allow]. An expectation that no longer fires fails the build, so a suppression cannot outlive the code that needed it.
-
Everything is documented
Every item, public or private, carries a doc comment, including fields, variants, test helpers and build scripts.
-
No unsafe
No crate may write unsafe code. The memory safety the broker relies on is the compiler's and its dependencies', never a hand-checked block.
-
Visibility and paths say what they mean
A pub item that cannot be reached from outside its crate is refused, so pub always means part of the crate's interface. Wildcard imports, needless qualification and long inline paths are refused, so every name used in a file is imported once, by its real path.
-
Pedantic lints as warnings
Clippy's pedantic group is on, along with lints for needless clones, collections built only to be consumed, and functions that could be const. Since warnings fail the build, these hold too.
Why it is enforced
A written rule about layers decays. Someone adds a dependency in a hurry, nobody notices, and a year later the boundary is a convention nobody follows. A rule that runs as a test can't decay quietly. The moment it is crossed, the build fails, and whoever crossed it has to fix the code or change the contract in the open.
For a user, the value is in the single owners. When q, a subscription and a registration all read a date-time through the same reader, a timestamp is accepted or refused the same way everywhere. When every SQL statement comes from one adapter, the collation and the typing rules of the storage schema hold for every query.