0002. .mli files for every domain module

Status: Accepted Date: 2026-04-16

#Context

OCaml allows a module without an .mli interface file to export everything defined in its .ml — all types, values, including implementation details. Early iterations of this project had wire-format conversion functions (status_of_wire, kind_to_wire) living in lib/domain/core/order.ml, visible to anyone who imported Order. When we started adding ACL adapters for individual brokers, these functions became a quiet coupling point: the domain type had Finam-shaped converters attached.

The problem surfaced when BCS entered the picture. Its order status enum is entirely different (numeric strings "1"..."8" vs. Finam's ORDER_STATUS_*). We couldn't have both sets of converters in Order without namespace-prefixing them, which was ugly and still let them leak. The right answer was: those converters don't belong in the domain at all; they're ACL translators.

But once we'd moved them out, nothing prevented the same mistake from recurring. A future contributor could add an order_from_grpc_enum helper to order.ml and it would just silently become part of the public API.

#Decision

Every .ml file in lib/domain/ has a matching .mli file. The interface is curated: it exposes the types, constructors, observers, and predicates the domain actually wants downstream code to see. Implementation details, wire-format helpers, or internal transformations live only in .ml.

Enforcement is structural:

#Alternatives considered

#Relying on naming conventions

"Just don't put wire converters in the domain." Works until it doesn't. We'd already seen it fail once.

#Strict sub-module isolation

Put wire converters inside a nested module (module Wire : ... end) within the domain module. Sub-modules are still exported; the only way to hide them without an .mli is to make them module-private via let _ = ..., which is even uglier.

#.mli only for "public" libraries, .ml-only for "internal"

Creates two tiers of modules with different rules. The line between "public" and "internal" is subjective and the rule erodes. Uniform policy is easier to enforce.

#Consequences

Easier:

Harder:

To watch for:

#Consequences observed since adoption

After adding .mli to all 35 domain modules (the second renovation), we caught three latent leaks: wire helpers, a mutable internal cache, and a raw Yojson value returned from what should have been a pure decoder. None of these would have been caught by tests — the tests used the "correct" parts of the API and didn't accidentally invoke the leaked ones.

#References