Skip to content

Project layout ​

Every Alveolus project has the same shape: the same folders, the same file names, the same direction between layers. Whoever opens the code, a teammate or an agent, knows where a concept lives before searching for it, and alveolus arch check keeps it that way.

src/ordering/ · a bounded contextcomposition rootordering.module.ts · wires it alldriving/controllersconsumersjobs, CLI…calls use casesdriven/repositoriesAPI clientsoutbox, clock…implements portsapplication/commands · queriesdomain/aggregates · entitiesvalue objects · eventserrors · services · portspublished-language/what other contexts read

Arrows read "depends on". They all point inwards: the domain depends on nothing but itself, so the business rules never change because a database, a framework or another context did.

At a glance ​

src/
  main.ts                     # starts the application
  app.module.ts               # assembles the bounded contexts
  ordering/                   # a bounded context
    ordering.module.ts        # its composition root
    domain/
      aggregates/             # order.aggregate.ts
      entities/               # order-line.entity.ts
      value-objects/          # money.value-object.ts, order-id.identifier.ts
      events/                 # order-placed.event.ts
      errors/                 # invalid-total.error.ts
      services/               # shipping-cost.service.ts
      repositories/           # orders.repository.ts
      ports/                  # price-list.port.ts
      views/                  # order-summary.view.ts
    application/
      commands/               # place-order.command.ts
      queries/                # get-order-summary.query.ts
      translators/            # order-events.translator.ts
    published-language/       # order-placed.representation.ts
    driven/
      pg/
        adapters/             # pg-orders.adapter.ts
      catalog/
        adapters/             # catalog-price-list.adapter.ts
    driving/
      http/
        controllers/          # orders.controller.ts
      rabbitmq/
        consumers/            # payment-received.consumer.ts
  catalog/                    # another bounded context, same shape
  shared-kernel/              # shared by every bounded context, same shape

Only create a folder when it gets its first file: a small context may have no entities/, services/ or driving/rabbitmq/.

Bounded contexts ​

A bounded context is a folder under src/, declared in alveolus.config.ts. Contexts may be nested, for instance under src/modules/. Each one has its own model: a Product in the catalog and a product in ordering are two different things, and neither imports the other.

A context is closed. The only class another context may import is an open host service, and only from an anti-corruption layer or from its composition root. What crosses the boundary is the published language: plain JSON that the reader redeclares on its side, never the classes of the other model.

src/catalog/CatalogApidriving/ · OpenHostServiceProductdomain/aggregates/src/ordering/CatalogPriceListdriven/ · AntiCorruptionLayerPriceListdomain/ports/importsextends✕never

The ordering domain asks for prices in its own words, through the PriceList port. The anti-corruption layer is the one place that knows the catalog exists: it calls CatalogApi, reads its JSON and answers with ordering's objects. If the catalog moves behind HTTP, only that adapter changes.

Layers ​

LayerHoldsMay import
domain/The model: aggregates, entities, value objects, events, errors, domain services, and the ports and repositories it needs.The domain of its context and of the shared kernel, the domain building blocks of @alveolus/core, the packages listed in domainDependencies. No framework, no ORM.
application/One class per use case: command handlers, query handlers, and the translators that turn domain events into the published language.The domain, the application, its own published language, @alveolus/core, domainDependencies, applicationDependencies. No framework.
published-language/The JSON types exchanged with other contexts: what this context publishes, and what it reads from the others.Its own published language, the published-language types of core, and packages such as a schema library.
driven/The adapters that implement the ports: database repositories, API clients, the outbox, the clock.The domain, the application, the published language, any package. Never driving/.
driving/The adapters that call the use cases: HTTP controllers, message consumers, scheduled jobs, CLI commands.The domain, the application, the published language, any package. Never driven/.

Inside driven/ and driving/, files always sit under the name of their technology: driven/pg/adapters/, driven/http/adapters/, driving/http/controllers/. Replacing a technology then means adding a folder next to the old one, never touching it. An adapter that calls another bounded context in the same process sits under the name of that context: driven/catalog/adapters/. Inside domain/ and application/, every class extends a building block of @alveolus/core: there are no free functions and no plain classes.

Folders and file names ​

Each class goes in the folder of its kind, in a file whose name ends with that kind. One class per file; the types that belong to it, such as its snapshot or its command input, stay in its file.

KindExtendsFolderFile name
AggregateAggregateRootdomain/aggregates/order.aggregate.ts
EntityEntitydomain/entities/order-line.entity.ts
Value objectValueObjectdomain/value-objects/money.value-object.ts
IdentifierIdentifierdomain/value-objects/order-id.identifier.ts
Domain eventDomainEventdomain/events/order-placed.event.ts
Domain errorDomainErrordomain/errors/invalid-total.error.ts
Domain serviceDomainServicedomain/services/shipping-cost.service.ts
RepositoryCommandRepository, QueryRepositorydomain/repositories/orders.repository.ts
PortPortdomain/ports/price-list.port.ts
Viewa View<…> typedomain/views/order-summary.view.ts
Command handlerCommandHandlerapplication/commands/place-order.command.ts
Query handlerQueryHandlerapplication/queries/get-order-summary.query.ts
Event translatorEventTranslatorapplication/translators/order-events.translator.ts
Representationa PublishedLanguage<…> typepublished-language/order-placed.representation.ts
Driven adaptera portdriven/<technology>/adapters/pg-orders.adapter.ts
Open host serviceimplements OpenHostServicedriving/<technology>/free

Tests sit next to the code they test and keep its name: order.aggregate.spec.ts or order.aggregate.test.ts.

Composition root ​

Each bounded context has one file at its root that wires its adapters into its use cases, its module: ordering.module.ts. It is a class that builds everything with new, or the module of your framework's container, such as a NestJS @Module: see Integrations. It is the only file that sees every layer, and the only one, besides an anti-corruption layer, that may import from another context: another context's module, to reach its open host services.

At the root of src/, main.ts starts the application and app.module.ts builds or imports the module of each bounded context. These files import composition roots only.

Shared kernel ​

src/shared-kernel/ holds what every bounded context needs in the same form: value objects such as Money, ports such as a tracer, and their adapters. It has the same layers as a bounded context, possibly grouped by feature (shared-kernel/time/driven/system/adapters/). Every context may import it; it imports none of them.

Keep it small: each change to the shared kernel reaches every context. Clock and IdGenerator already come with @alveolus/core; only their adapters live here.

Who may import what ​

Read a row as "files in this layer may import…", within the same bounded context or from the shared kernel.

From ↓ · To →domainapplicationpublished-languagedrivendrivingcomposition root
domain✓✕✕✕✕✕
application✓✓✓✕✕✕
published-language✕✕✓✕✕✕
driven✓✓✓✓✕✕
driving✓✓✓✕✓✕
composition root✓✓✓✓✓✓

From another bounded context, only an open host service may be imported, by an anti-corruption layer or the composition root.

Checked for you ​

alveolus arch check verifies this layout on every run: see the rules, one page per rule, with what each one checks and why.

Released under the MIT License.