Anti-corruption layers
An anti-corruption layer translates another bounded context into the language of yours, so that the foreign model never enters your domain or your application. It is the adapter that touches the other context, and the only one allowed to.
export class CatalogPriceList extends PriceList implements AntiCorruptionLayer {
async priceOf(productId: ProductId): Promise<Money | undefined> {
const product: CatalogProductRepresentation | undefined = await this.catalog.productById(productId.value);
return product === undefined ? undefined : this.toMoney(product.price);
}
}When to use
Write one on every adapter that reads another bounded context:
- a driven adapter, when your application needs something from the other context, such as a price from the catalog;
- a driving adapter, when the other context sends you something, such as an
OrderPlacedevent.
Usage
Declare what your context needs
The domain states the need in its own words, as a port. Nothing in it mentions the catalog.
import { Port } from "@alveolus/core";
import type { Money } from "../../../shared-kernel/domain/value-objects/money.value-object";
import type { ProductId } from "../value-objects/product-id.identifier";
export abstract class PriceList extends Port {
abstract priceOf(productId: ProductId): Promise<Money | undefined>;
}Translate in a driven adapter
The adapter extends the port and implements AntiCorruptionLayer. It lives under the name of the context it calls, driven/catalog/adapters/. It reads the representation your context declares, and returns your objects.
import type { AntiCorruptionLayer } from "@alveolus/core";
import { CatalogApi } from "../../../../catalog/driving/in-process/catalog-api";
import { Money } from "../../../../shared-kernel/domain/value-objects/money.value-object";
import { PriceList } from "../../../domain/ports/price-list.port";
import type { ProductId } from "../../../domain/value-objects/product-id.identifier";
import type { CatalogProductRepresentation } from "../../../published-language/catalog-product.representation";
export class CatalogPriceList extends PriceList implements AntiCorruptionLayer {
constructor(private readonly catalog: CatalogApi) {
super();
}
async priceOf(productId: ProductId): Promise<Money | undefined> {
const product: CatalogProductRepresentation | undefined = await this.catalog.productById(productId.value);
if (product === undefined) {
return undefined;
}
const price = Money.of(product.price.amount, product.price.currency);
if (!price.ok) {
throw new Error(`The catalog sent an invalid price: ${price.error.type}`);
}
return price.value;
}
}Typing the answer with your own CatalogProductRepresentation makes TypeScript check that the catalog still sends the fields you read. If it moves behind HTTP, only this adapter changes: it fetches and validates the JSON instead of calling CatalogApi.
Translate in a driving adapter
When the other context sends you a message, the anti-corruption layer is the consumer, in driving/. It turns the event into one of your commands.
import type { AntiCorruptionLayer } from "@alveolus/core";
import { OpenInvoiceHandler } from "../../../application/commands/open-invoice.command";
import type { OrderPlacedRepresentation } from "../../../published-language/order-placed.representation";
export class OrderPlacedConsumer implements AntiCorruptionLayer {
constructor(private readonly openInvoice: OpenInvoiceHandler) {}
async consume(event: OrderPlacedRepresentation): Promise<void> {
const opened = await this.openInvoice.handle({ amount: event.payload.total, orderId: event.payload.orderId });
if (!opened.ok) {
throw new Error(`Cannot open the invoice of order ${event.payload.orderId}: ${opened.error.type}`);
}
}
}Events are delivered at least once: ignore an event whose id you have already handled.
Keep the other model at the edge
import { CatalogApi } from "../../../catalog/driving/in-process/catalog-api";import { PriceList } from "../../domain/ports/price-list.port";Why?
Once the application calls the catalog directly, its representations spread into use cases and then into the domain, and every change in the catalog becomes a change in ordering. With the anti-corruption layer, representations come in through one class and never go further: what leaves it are your own objects. bc-isolation reports any import of another context outside an anti-corruption layer or the composition root.
Reference
abstract class AntiCorruptionLayerAntiCorruptionLayer has no members. Apply it with implements, because a driven anti-corruption layer already extends its port: it marks the class as the translator of another bounded context.
Caveats
- Representations come in, never go out: the public methods return your objects.
- Name the port after your language (
PriceList), not after the other context (CatalogClient). - An answer that breaks the published language is a technical failure: throw, do not return a domain error.
- A driven anti-corruption layer is placed like any adapter, in
driven/<context>/adapters/; a consumer goes indriving/: seeplacement.
Import from @alveolus/core or @alveolus/core/anti-corruption-layers.
See also
- Open host services, what a driven anti-corruption layer calls
- Published Language, what it reads
- Ports, what a driven anti-corruption layer extends
bc-isolation