PouryaKit
Concepts

Host and Module

A Host is the application you deploy; a Module is a business boundary inside it, reached only through its Gateway.

What it is

A Host is the application you deploy: one process that runs Operations. A Module is a business boundary inside a Host. It owns its data, its rules and its public Contracts, the messages it publishes and the questions it answers.

Inside a Module, code is grouped by Business Area, one aggregate or a close group of them, and each Business Area holds its Features: one command or query with everything needed to serve it. One Host holds one container for all of its Modules. What keeps the Modules apart is the compiler and the structure tests: everything in a Module stays internal except its Composition Root and a few named kinds of type, and a Module references another only through that Module's Contracts.

Modules of one Host never call each other over HTTP

Inside a Module, aggregates talk through Domain Events. Across Modules, a Module tells the others what happened with an Integration Event, or keeps a copy of their data from Snapshots. It asks another Module a question directly, through that Module's Contracts, only when the Operation cannot go on without the answer.

When it matters

  • One business boundary: start with the monolith template, one Module's data and rules in one Host. It grows into the modular monolith when a second Module arrives, without changing its business code. See Templates and Choose a template.
  • Several boundaries in one process: the modular monolith, each Module reached through its Gateway.
  • One Module in a process of its own: the service template, which receives another Host's Integration Events and Snapshots over RabbitMQ.

How it works

Each Module declares everything it needs and offers in one Composition Root: a public sealed class that implements IModuleDefinition. Its Configure method declares the Module's data, its Features, what it publishes and subscribes to, and its endpoints, on a ModuleBuilder. The Host adds each Module once, with AddPouryaKitModules. The Composition Root section shows the Proving Sample's Catalog and the Host that composes it.

Anything outside a Module asks it to run a command or query through its Gateway, IGatewayProcessor<TModule>. Each call is one Operation in a scope of its own, run with the Operation Context the caller passes. An endpoint is the most common caller. This one, from the Proving Sample's Inventory Module, sends a query through Inventory's Gateway:

GetStockItemEndpoint.cs
// No :guid route constraint: an id that does not parse is the caller's mistake (400), not a missing route (404).
internal sealed class GetStockItemEndpoint : IEndpoint
{
    public void Map(IEndpointRouteBuilder routes) =>
        routes.MapGet(
            "/stock-items/{productId}",
            async (
                Guid productId,
                OperationContext context,
                IGatewayProcessor<InventoryModule> inventory,
                CancellationToken cancellationToken
            ) =>
                (
                    await inventory.QueryAsync(
                        new GetStockItemQuery(productId),
                        context,
                        cancellationToken
                    )
                ).ToHttpResult()
        );
}

When another Module needs an answer before its Operation can go on, the owning Module publishes the question as an interface in its Contracts and answers it through its own Gateway. Asking another Module a question walks through how Catalog answers product questions.

What the Kit guarantees

GuaranteeStated in
What a Module may make public, and what it may referenceStructure tests
Which Module's Gateway runs a command or queryWhich Module an Operation belongs to
One Operation per Gateway call, in its own scopeThe Gateway
How Modules talk to each otherAsking another Module a question
What a mistake in a Host's composition doesThe Host's checks

Failure messages

Next steps

On this page