Open host services
An open host service is the documented entry point of a bounded context: the class other contexts call, answering in the published language, never with objects of the domain. It is the only class another context may import.
import type { OpenHostService } from "@alveolus/core";
export class CatalogApi implements OpenHostService {
async productById(productId: string): Promise<ProductRepresentation | undefined> { … }
}When to use
Write one when other bounded contexts or services need to ask yours something, whatever the transport: a class called in the same process, an HTTP API, an RPC endpoint. When your context only tells the others what happened, publishing integration events is enough.
Usage
Expose a class in the same process
In a modular monolith, the open host service is a class of driving/. It calls the application and maps what it reads to the published language.
import type { OpenHostService } from "@alveolus/core";
import { GetProductHandler } from "../../application/queries/get-product.query";
import type { ProductRepresentation } from "../../published-language/product.representation";
export class CatalogApi implements OpenHostService {
constructor(private readonly getProduct: GetProductHandler) {}
async productById(productId: string): Promise<ProductRepresentation | undefined> {
const product = await this.getProduct.handle({ productId });
if (!product.ok) {
return undefined;
}
const { id, name, price } = product.value;
return { id: id.value, name, price: { amount: price.amount, currency: price.currency } };
}
}Other contexts import this class, and only this class, from an anti-corruption layer.
Wire it in the composition root
The composition root of the bounded context builds its open host services and exposes them, and nothing else. The composition root of the downstream context receives them to build its anti-corruption layers.
export class CatalogModule {
readonly api: CatalogApi;
constructor(db: Pool) {
this.api = new CatalogApi(new GetProductHandler(new PgProducts(db)));
}
}export class OrderingModule {
constructor(db: Pool, catalog: CatalogModule) {
const prices = new CatalogPriceList(catalog.api);
…
}
}Exposing a repository or a handler would let another context reach the model behind the open host service. With NestJS, the module exports the open host service only: see NestJS.
Expose it over HTTP
The same role fits a controller: it implements OpenHostService and answers with a representation. The routes are written with your HTTP framework; see Integrations.
import type { OpenHostService } from "@alveolus/core";
import type { CatalogApi } from "../../in-process/catalog-api";
export class ProductsController implements OpenHostService {
constructor(private readonly catalog: CatalogApi) {}
async product(productId: string) {
const product = await this.catalog.productById(productId);
if (product === undefined) {
return { status: 404, body: { error: "ProductNotFound", productId } };
}
return { status: 200, body: product };
}
}The catalog can then move to its own service: the HTTP contract is already the published language.
Answer in the published language
async productById(productId: string): Promise<Product | undefined> {
return this.products.findById(new ProductId(productId));
}async productById(productId: string): Promise<ProductRepresentation | undefined> {
const product = await this.getProduct.handle({ productId });
return product.ok ? this.toRepresentation(product.value) : undefined;
}Why?
An open host service that returns an aggregate hands the model to every caller: they start depending on its methods and fields, and the catalog can no longer change them. The representation is the contract; the model stays behind it.
Reference
abstract class OpenHostServiceOpenHostService has no members. Apply it with implements, so the class stays free to extend something else: it marks the class as the documented entry point of its bounded context.
Caveats
- It is the only class another bounded context may import, and only from an anti-corruption layer or from its composition root: see
bc-isolation. - It lives in
driving/, under the name of its technology: seeplacement. - Type what it returns with a representation, even inside an HTTP response: the representation is the contract.
Import from @alveolus/core or @alveolus/core/open-host-services.
See also
- Published Language, what it answers in
- Anti-corruption layers, how another context reads it
- Project layout: bounded contexts
bc-isolation