Skip to main content
A microservice is composed exactly like a module: it exports a single <PascalName>Module of type ModuleType from @ooneex/module that declares what the service owns. You list artifact classes in the appropriate array, and the framework resolves and wires each one through the dependency injection container — controllers join the router, middleware joins the pipeline, cron jobs join the scheduler, events join the bus, and entities join the data source. The one thing that sets a microservice apart is exposure. A plain module is registered into AppModule/SharedModule so the app process can resolve it. A microservice is not — it runs as its own process, with its own DI container, its own port, and its own Dockerfile, and its src/index.ts entrypoint boots the service directly from its own module. Composition is identical; what changes is who runs it.

How it works

The service’s ModuleType has one array per artifact kind. Listing a class is the declaration — within the microservice’s process the framework reads the module, registers each class with the container, and applies its decorators.
import type { ModuleType } from "@ooneex/module";

export const BillingModule: ModuleType = {
  controllers: [],   // HTTP and WebSocket controllers
  entities: [],      // database entities (TypeORM)
  middlewares: [],   // request/socket middleware
  cronJobs: [],      // scheduled jobs
  events: [],        // pub/sub event handlers
};
ArrayWhat belongs here
controllersHTTP controllers from @ooneex/controller and WebSocket controllers from @ooneex/socket. The service registers a route for each @Route.* decorator.
entitiesTypeORM entities the service persists in its own database.
middlewaresRequest and socket middleware that intercept the service’s traffic, registered via @decorator.middleware().
cronJobsScheduled jobs the service runs on a timer within its process.
eventsPub/sub event handlers the service reacts to or emits.
Every class you list is resolved through the microservice’s DI container. That is what makes a bare array entry enough: the container constructs the class, injects its dependencies, and the framework reads its decorators to plug it into routing, the scheduler, or the event bus — the same resolution model as any Ooneex app, just running in a process of its own.

Composing the service module

Build the module up one array at a time. Each array maps to a kind of artifact the service owns — nothing more. A complete BillingModule for a self-contained billing service looks like this.

controllers

Both HTTP and WebSocket controllers go in controllers. The service registers each one’s routes from its @Route.* decorators.
import { InvoiceCreateController } from "./controllers/InvoiceCreateController";
import { InvoiceListController } from "./controllers/InvoiceListController";

export const BillingModule: ModuleType = {
  controllers: [InvoiceCreateController, InvoiceListController],
  entities: [],
  middlewares: [],
  cronJobs: [],
  events: [],
};

entities

List the TypeORM entities the service persists. A microservice owns its own database, so these entities back its data source — not a shared one.
import { InvoiceEntity } from "./entities/InvoiceEntity";
import { PaymentEntity } from "./entities/PaymentEntity";

export const BillingModule: ModuleType = {
  controllers: [InvoiceCreateController, InvoiceListController],
  entities: [InvoiceEntity, PaymentEntity],
  middlewares: [],
  cronJobs: [],
  events: [],
};

middlewares

Middleware that intercepts the service’s requests or socket connections goes here, registered via its @decorator.middleware().
import { BillingAuthMiddleware } from "./middlewares/BillingAuthMiddleware";

export const BillingModule: ModuleType = {
  controllers: [InvoiceCreateController, InvoiceListController],
  entities: [InvoiceEntity, PaymentEntity],
  middlewares: [BillingAuthMiddleware],
  cronJobs: [],
  events: [],
};

cronJobs

Scheduled work the service runs on a timer belongs in cronJobs. Each job is registered with the scheduler from its cron expression — and runs inside this service’s process.
import { InvoiceSweepCron } from "./crons/InvoiceSweepCron";

export const BillingModule: ModuleType = {
  controllers: [InvoiceCreateController, InvoiceListController],
  entities: [InvoiceEntity, PaymentEntity],
  middlewares: [BillingAuthMiddleware],
  cronJobs: [InvoiceSweepCron],
  events: [],
};

events

Pub/sub handlers the service reacts to — or emits — go in events. Each is registered as a subscriber on the event bus.
import { InvoicePaidEvent } from "./events/InvoicePaidEvent";

export const BillingModule: ModuleType = {
  controllers: [InvoiceCreateController, InvoiceListController],
  entities: [InvoiceEntity, PaymentEntity],
  middlewares: [BillingAuthMiddleware],
  cronJobs: [InvoiceSweepCron],
  events: [InvoicePaidEvent],
};
The result is a complete vertical slice: one object that declares every artifact the billing service owns, with no wiring code in between.

Booting from the service’s own module

This is where a microservice diverges from a plain module. A module declares its artifacts and is then registered into a destination module so the app process can resolve them — see module composition, where OrderModule is spread into AppModule. A microservice skips that step entirely. It is never registered into AppModule or SharedModule. Instead, the template wires the microservice’s src/index.ts entrypoint to boot the service from its own module, so the service’s process resolves BillingModule directly.
// src/index.ts — the microservice's own entrypoint
import { BillingModule } from "./BillingModule";

// The template boots the service from its own module,
// not from AppModule. This process gets its own DI
// container and listens on the port from .env.yml.
At startup, src/OnAppStart.ts runs so the service can perform any setup it needs (connecting to its data source, warming caches) before it begins handling traffic. From there, resolution flows through the microservice’s DI container exactly as it would in any Ooneex app: every class listed in BillingModule is registered with the container, and the framework applies the decorators — controllers join the router (see routing), middleware joins the pipeline, cron jobs join the scheduler, events join the bus, and entities join the data source.

Best practices

  • One bounded context per service. A microservice is a vertical slice that owns a single domain end to end. Keep billing in BillingModule; if a concern belongs to another domain, it belongs to that domain’s service.
  • Keep the service self-contained. Declare the service’s own entities and let it own its database. A microservice should not reach into another service’s data source or share tables.
  • Communicate over the network, not by importing. When the billing service needs data from another service, call it over the network — see networking. Do not import another service’s controllers, entities, or repositories.
  • List only what the service owns. An array entry is a declaration of ownership. Resolution happens inside this process, so every class you list must be one this service actually runs.
  • Don’t register the service into AppModule. A microservice boots from its own module by design. Wiring it into AppModule defeats the isolated-process model.
  • Prefer the generators. Scaffolding with the :create commands (controller:create, service:create, entity:create, …) targets the microservice’s module and scaffolds into its src/, keeping the module in sync without manual edits.

Learn more