cohort reaches one external system — a Brevo list — through two ports, and one of them is not a port at all.
CohortPort (with CohortPortRegistry, BrevoCohortAdapter, MockCohortPort) and TargetStrategy (with TargetStrategies, BrevoTargetStrategy, MockTargetStrategy) both abstract the same operations on the same list: create it, add and remove members, delete it, list what is on it. Each has exactly one production implementation, and both delegate to the same ContactListAdapter bean. Their member types — MemberRef(externalUserId, label) and ExternalMember(externalUserId, label) — are structurally identical. Both registries dispatch on TargetSystem, whose only cohort-valid value is BREVO: GOOGLE_CALENDAR has neither a CohortPort nor a TargetStrategy, so require(system) is a lookup with one possible answer.
Two consumers use CohortPortRegistry; five use TargetStrategies.
The part that is not a port
BrevoTargetStrategy injects the raw ContactsApi alongside the ContactListAdapter it also uses, and imports three Brevo model types directly, for folder and list paging. api ADR-019 forbids an external model reaching a domain, and cohort's Modulith whitelist grants it only contact :: api — but Modulith does not police third-party packages, so it verifies clean.
#1158 added VendorClientArchitectureTest, which now catches this. The violation is pinned there with a note naming the fix:
Removing these means publishing the catalog side through contact :: api so cohort speaks only to the wrapper.
ArchUnit sees six reaches where git grep sees three, because the return types count too.
Also worth settling
The two ports disagree about what an external id is. ContactListAdapter types every id as Long and its comment justifies a signature in Brevo's terms; CohortPort explicitly rejects that — "ids are String so Discord snowflakes and Google group emails sit alongside Brevo's numeric list ids" — and the codebase pays for the disagreement with a private String.toBrevoId() in two places. Worse, the numeric assumption is hard-coded into the generic base: ContactAdapterSyncTarget calls currentExternalId!!.toLong(), so a non-numeric system would break the shared class rather than its own adapter.
Suggested shape
Collapse CohortPort into TargetStrategy — the richer of the two, since it already has folders, move, catalog and capabilities — and push folder and list paging down into contact's adapter so cohort never imports a vendor package. That empties the pin in VendorClientArchitectureTest, which is the check that this actually landed.
Acceptance criteria
cohortreaches one external system — a Brevo list — through two ports, and one of them is not a port at all.CohortPort(withCohortPortRegistry,BrevoCohortAdapter,MockCohortPort) andTargetStrategy(withTargetStrategies,BrevoTargetStrategy,MockTargetStrategy) both abstract the same operations on the same list: create it, add and remove members, delete it, list what is on it. Each has exactly one production implementation, and both delegate to the sameContactListAdapterbean. Their member types —MemberRef(externalUserId, label)andExternalMember(externalUserId, label)— are structurally identical. Both registries dispatch onTargetSystem, whose only cohort-valid value isBREVO:GOOGLE_CALENDARhas neither aCohortPortnor aTargetStrategy, sorequire(system)is a lookup with one possible answer.Two consumers use
CohortPortRegistry; five useTargetStrategies.The part that is not a port
BrevoTargetStrategyinjects the rawContactsApialongside theContactListAdapterit also uses, and imports three Brevo model types directly, for folder and list paging. api ADR-019 forbids an external model reaching a domain, andcohort's Modulith whitelist grants it onlycontact :: api— but Modulith does not police third-party packages, so it verifies clean.#1158 added
VendorClientArchitectureTest, which now catches this. The violation is pinned there with a note naming the fix:ArchUnit sees six reaches where
git grepsees three, because the return types count too.Also worth settling
The two ports disagree about what an external id is.
ContactListAdaptertypes every id asLongand its comment justifies a signature in Brevo's terms;CohortPortexplicitly rejects that — "ids areStringso Discord snowflakes and Google group emails sit alongside Brevo's numeric list ids" — and the codebase pays for the disagreement with a privateString.toBrevoId()in two places. Worse, the numeric assumption is hard-coded into the generic base:ContactAdapterSyncTargetcallscurrentExternalId!!.toLong(), so a non-numeric system would break the shared class rather than its own adapter.Suggested shape
Collapse
CohortPortintoTargetStrategy— the richer of the two, since it already has folders, move, catalog and capabilities — and push folder and list paging down intocontact's adapter socohortnever imports a vendor package. That empties the pin inVendorClientArchitectureTest, which is the check that this actually landed.Acceptance criteria
cohortimports nothing fromnet.blueshell.clients, and the pinned entry inVendorClientArchitectureTestis gone rather than editedtoBrevoIdhas one definition or none