A type from someone else's library that ends up in your public signature becomes your contract: consumers compile against it, and your upgrade of that library is their breaking change. The leak is silent because no compiler asks “do you really want to expose this?”; per Hyrum's Law (16), the answer turns up when it is already too late to break anything.
The check is mechanical: list every type mentioned in the module's public surface. Anything that is not in your own code is someone else's contract you have silently taken on supporting. Then comes a choice of three, and it must be deliberate: wrap the foreign type in your own (the dependency then stays a secret, 12), declare the transitivity explicitly and accept it as part of the API, or remove the type from the signature. The worst option is the fourth, the default one: choosing nothing.
module billing.api {
// I need it, consumers can't see it: an implementation detail
requires com.fasterxml.jackson.databind;
// a type from billing.model appears in my public signatures,
// so consumers must be able to see it. transitive is an admission:
// “billing.model compatibility is now my problem”
requires transitive billing.model;
exports com.acme.billing.api;
exports com.acme.billing.spi to billing.plugins; // qualified export
// reflection is granted narrowly and by name, not to all comers
opens com.acme.billing.dto to com.fasterxml.jackson.databind;
// the extension point is declared, not found by scanning the classpath
uses com.acme.billing.spi.TaxProvider;
provides com.acme.billing.spi.TaxProvider with com.acme.billing.tax.DeTaxProvider;
}// pub use: same trick, same price; a foreign type becomes your API,
// and a major bump of chrono becomes a major bump of your crate
pub use chrono::NaiveDate;
// a boundary instead of a leak: the newtype keeps chrono an implementation detail
pub struct SettlementDate(NaiveDate);
// a pub field of a pub struct is a contract too; non_exhaustive keeps
// the right to add fields without breaking consumers
#[non_exhaustive]
pub struct Settlement { pub date: SettlementDate }// Go has neither requires transitive nor re-exports: transitivity leaks
// through the signature, the only place it is visible at all. The consumer
// has to import lib/pq to inspect this error
func Charge(ctx context.Context, id string) (*pq.Error, error)
// the boundary: the foreign error is wrapped in ours; the contract is errors.Is
var ErrDuplicate = errors.New("billing: duplicate charge")
func Charge(ctx context.Context, id string) error {
if err := insert(ctx, id); isUniqueViolation(err) {
return fmt.Errorf("charge %s: %w", id, ErrDuplicate) // lib/pq stays inside
}
return nil
}# the same obligation, only declared by convention rather than by the compiler
from ._settlement import Settlement
from ._vendor_client import Client as Client # redundant alias = “this is API” (PEP 484)
# a second, more explicit layer of the same statement: Client is promised publicly,
# so a vendor upgrade is our breaking change, not theirs
__all__ = ["Settlement", "Client"]The flip side of the same principle is extension points. uses/provides declare a service in the module contract instead of scanning the classpath at startup: the implementation is plugged in late, but the fact of extensibility is declared early and visible in the artifact. The same choice comes up wherever there are plugins: the set of extension points is part of the public surface, and adding them is cheaper than removing them.