DAG Package Discipline
Arda backend modules organize Kotlin source into packages that form a directed acyclic graph (DAG). The rule applies at every aggregation level: no leaf package may import a package that (directly or transitively) imports it back, and no aggregate (a package and everything beneath it) may form a cycle with another aggregate.
A leaf-level DAG is necessary but not sufficient. Two aggregates can each be internally acyclic while their union contains a cycle that crosses the boundary. Reviewing one level at a time misses this.
The two checks
Section titled “The two checks”For a module whose top-level package is cards.arda.<component>.<area>.<module>:
-
Leaf check. Build the directed graph where nodes are individual packages (e.g.
business,persistence,service,servers.postmark.shared,common.lib.util.email) and edges areimportstatements that cross a leaf-package boundary. The graph must have no cycles. -
Aggregate check. Group leaves by every prefix. For each grouping (e.g.
servers.*,business,common.lib.util.*), collapse all leaves under that prefix into a single node and re-check. Every collapsed graph must also be acyclic.
The two checks are independent. A module that passes (1) can still fail (2) when an aggregate boundary “wraps” a backward edge that was legal at the leaf level.
Layering inside a module
Section titled “Layering inside a module”Most module-internal aggregates fit one of these roles. They sort top-down — packages at the top may depend on those below them, never the reverse.
| Layer | Typical packages | Imports |
|---|---|---|
| Wiring / entry point | Module.kt at module root | All layers below |
| Protocol / API | api/rest/, api/proto/ | service, business |
| Service | service/ | business, persistence, servers/*/shared, common.lib.util.* |
| Persistence | persistence/ | business, common.lib.util.* |
| Business / domain | business/ | common.lib.util.*, servers/*/shared |
| Servers (external systems) | servers/<vendor>/shared/, servers/<vendor>/<proxy>/ | common.lib.util.* only |
| Common-module staging | common.lib.util.*, common.lib.util.<concept>.* | Nothing module-internal |
The two layers most likely to invite cycles are business and servers: it is tempting for a vendor-shared package to import a business type, then for a business type to reference a vendor-shared shape. Both directions are wrong. Vendor packages depend only on common.lib.util.*; business types reference vendor packages by name when the field is genuinely vendor-shaped (e.g. a Postmark server ID), but the concept sits in business and the shape sits in servers.
The common.lib.util.* staging convention
Section titled “The common.lib.util.* staging convention”cards.arda.<component>.common.lib.util.* is a staging area for types that will likely be promoted to cards.arda.common.lib.* in the central common-module at a later date. Place a type here when:
- It has no module-internal dependencies (only stdlib + serialization +
cards.arda.common.lib.*). - Its shape is generic — not tied to one module’s vocabulary (
DnsRecord,TokenCipherEnvelope,EmailAddress). - It is referenced by more than one leaf inside the module, or is on a clear path to being shared across modules.
This keeps such types out of business/ (which would couple business to infrastructure types) and out of servers/<vendor>/ (which would couple all vendors to one vendor’s path). Sub-packages cluster related types: common.lib.util.email.LocalPart, common.lib.util.email.EmailAddress.
When the type is later promoted to common-module, every import inside the module changes prefix in one mechanical rename; no source restructuring is required.
Detecting cycles
Section titled “Detecting cycles”A quick check at the leaf level on a Kotlin source tree:
# Build a flat list of package → package edges from import statementsfind src/main/kotlin/cards/arda/<component>/<area>/<module> -name '*.kt' \ -exec awk ' /^package / { pkg = $2 } /^import / && $2 ~ /^cards\.arda\./ { sub(/\.[^.]+$/, "", $2) # drop class name if ($2 != pkg) print pkg " -> " $2 } ' {} \;Pipe the output into any graph tool (or a 20-line Python script using networkx) and ask for the strongly-connected components — anything larger than one node is a cycle.
For the aggregate check, repeat with every prefix folded:
# pseudoedges_leaf = parse_edges("imports.txt")for depth in range(1, max_depth(edges_leaf)): edges_at_depth = {(fold(a, depth), fold(b, depth)) for a, b in edges_leaf} edges_at_depth = {(a, b) for a, b in edges_at_depth if a != b} assert is_acyclic(edges_at_depth), f"cycle at depth {depth}"Run both checks before every PR; landing a cycle makes future refactoring exponentially harder.
When the rule pushes back on a design
Section titled “When the rule pushes back on a design”If you cannot place a type without creating a cycle, the design is wrong — not the rule. Common signals:
- A type with both business meaning and vendor shape (e.g. “the encrypted Postmark token”). Split: the concept (encrypted token envelope) lives in
common.lib.util/; the vendor wrapper lives inservers/postmark/shared/and composes the envelope. - A “helper” file that pulls together pieces from multiple layers. The helper itself wants to live above all of them — promote to
service/or the wiring layer; do not let it sit beside one of its sources. - A persistence-side type that “needs” to know a business-state enum. The business enum is upstream of persistence; passing the value into persistence at write time keeps the edge in the right direction.
Worked references
Section titled “Worked references”cards.arda.operations.shopaccess.email.*— leaf-level acyclic and aggregate-level acyclic acrossbusiness,persistence,service,servers.postmark.{shared,resources,signature,server,webhook},common.lib.util.{,email}, andapi/rest. Phase 5b S01+S02 refactor.cards.arda.operations.reference.item.*— established layering pattern for a Data Authority module; the canonical reference for business/persistence/service split.
Related pages
Section titled “Related pages”- Module Concept — the architectural definition of a Module.
- Naming Conventions — canonical names and forms.
- Information Model Design — entity package layout per Data Authority module.
Copyright: © Arda Systems 2025-2026, All rights reserved