Skip to content

Core Architecture

1. Overview

@nx/core implements a layered architecture built on top of the IGNIS Framework. Every backend microservice in the BANA monorepo extends DefaultApplication from this package, inheriting a consistent set of infrastructure concerns: authentication, CORS, health checks, Swagger documentation, database connectivity, and cross-service communication.

This document covers:

  • The layered architecture and how IGNIS primitives map to each layer
  • Application lifecycle and the DefaultApplication class
  • Dependency injection patterns
  • Component, controller, repository, and service patterns
  • Event-driven architecture (Redis Pub/Sub + BullMQ queues)
  • Bootstrap helpers for application and migration entry points
  • Cross-service communication via IdentityNetworkService
  • Authentication flow (JWT + Basic)
  • Package dependency chain

2. Layered Architecture

All backend services follow a five-layer architecture. Each layer has a single responsibility and communicates only with its immediate neighbors.

LayerComponentDescription
ApplicationDefaultApplicationpreConfigure() lifecycle · CORS · BodyLimit 100 MB · Swagger Scalar
Bootstrap HelpersbootstrapApplication() · bootstrapMigration() · createAppConfig() · createMigrationProcessLoader()
Built-in ComponentsHealthCheck GET /health · Swagger Scalar /explorer · Authenticate JWT stateless · Basic → Identity
Package ComponentsSignal · Payment · Commerce · Finance · Inventory · Search · Asset
ServiceBusiness ServicesDomain logic · Validation · Orchestration
IdentityNetworkServicesignIn() HTTP → Identity service · Cross-service Basic auth delegation
Event HandlersRedis Pub/Sub — Payment · Onboarding channels · IEventBus · RedisPubSubAdapter
Queue ConsumersBullMQ — 4 systems · 8 types · 3 partitions · Commerce · Finance · Inventory · Sale
BaseSocketEventServicebroadcast() · sendToRoom() · sendToClient() · Redis-backed cross-instance delivery
RepositorySoftDeletableRepositorySoft delete · Restore · Default filter deletedAt IS NULL auto-applied
DefaultCRUDRepositoryfind · create · updateById · deleteById · count · withTransaction
InfrastructurePostgresCoreDataSourceDrizzle ORM · node-postgres Pool · 7 schemas · 55 models
RedisConnectionFactoryCache · Pub/Sub · WebSocket · Single mode · Cluster mode
BullMQScheduler · Confirmation · Processing · 12 queues with hash-based partition routing
LayerIGNIS Base ClassResponsibility
ApplicationBaseApplicationLifecycle management, middleware setup, DI container root
ComponentBaseComponentFeature module registration (repositories, services, controllers)
ServiceBaseServiceBusiness logic, transaction orchestration, external communication
RepositoryDefaultCRUDRepositoryData access abstraction, CRUD operations, soft-delete
DataSourceBaseDataSourceDatabase driver configuration, connection pooling

3. Application Lifecycle

3.1 DefaultApplication

Every backend service extends DefaultApplication, which in turn extends the IGNIS BaseApplication. It provides a structured initialization flow via the preConfigure() lifecycle method.

typescript
export class DefaultApplication extends BaseApplication {
  protected applicationRoles: string[] = [];

  preConfigure() {
    this.applicationRoles = this.getApplicationRoles();

    this.configureDatasources();
    this.configureRepositories();
    this.configureServices();
    this.configureComponents();
    this.configureSecurity();
    this.configureControllers();
  }

  async postConfigure() {
    // Runs after all components are bound
  }
}

3.2 Lifecycle Sequence

The following diagram shows the full startup sequence, from the entry point (index.ts) through to a running HTTP server.

3.3 preConfigure() Method Order

The order of method calls inside preConfigure() is significant. Each step depends on the previous one being complete.

OrderMethodWhat It Does
1configureDatasources()Registers PostgreSQL connection pool (no-op by default, datasources are auto-discovered from glob)
2configureRepositories()Registers repository classes into the DI container (no-op by default, repositories are auto-discovered from glob)
3configureServices()Registers IdentityNetworkService for cross-service auth
4configureComponents()Binds HealthCheckComponent at /health and SwaggerComponent at /doc
5configureSecurity()Configures JWT and Basic authentication strategies via AuthenticateComponent
6configureControllers()Registers HTTP controllers (no-op by default, overridden per package)

3.4 Middleware Setup

setupMiddlewares() configures two global Hono middlewares:

MiddlewareConfiguration
CORSOrigin: *, all methods, 86400s max-age, credentials enabled
Body Limit100 MB max, returns 413 Content Too Large on overflow

3.5 Overriding in Downstream Packages

Each backend service creates its own Application class that extends DefaultApplication and overrides the relevant configure methods:

typescript
// packages/sale/src/application.ts
export class Application extends DefaultApplication {
  override preConfigure(): void {
    this.configureServices();
    this.configureComponents();
    this.configureSecurity();
  }

  override configureComponents(): void {
    super.configureComponents(); // HealthCheck + Swagger
    this.component(ApplicationSaleComponent);
  }

  override async postConfigure(): Promise<void> {
    // Package-specific post-boot logic
  }
}

4. Dependency Injection

The IGNIS DI container uses constructor injection with the @inject() decorator. Dependencies are identified by string-based binding keys.

4.1 Injection Pattern

typescript
import { inject, BaseService } from '@venizia/ignis';

export class SaleOrderService extends BaseService {
  constructor(
    @inject({ key: 'repositories.SaleOrderRepository' })
    private saleOrderRepository: SaleOrderRepository,

    @inject({ key: 'repositories.SaleOrderItemRepository' })
    private saleOrderItemRepository: SaleOrderItemRepository,
  ) {
    super({ scope: SaleOrderService.name });
  }
}

4.2 Binding Key Convention

Binding keys follow the pattern {namespace}.{ClassName}:

NamespaceExample KeyRegistered Via
repositoriesrepositories.SaleOrderRepositorythis.application.repository(SaleOrderRepository)
servicesservices.IdentityNetworkServicethis.application.service(IdentityNetworkService)
controllerscontrollers.SaleOrderControllerthis.application.controller(SaleOrderController)
datasourcesdatasources.PostgresCoreDataSourcethis.application.datasource(PostgresCoreDataSource)

You can also build keys explicitly using the IGNIS BindingKeys utility:

typescript
import { BindingKeys, BindingNamespaces } from '@venizia/ignis';

const key = BindingKeys.build({
  namespace: BindingNamespaces.REPOSITORY,
  key: 'SaleOrderRepository',
});
// Result: 'repositories.SaleOrderRepository'

4.3 Value Binding

For configuration objects, use the bind().toValue() pattern:

typescript
this.bind<IHealthCheckOptions>({
  key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS,
}).toValue({
  restOptions: { path: '/health' },
});

5. Component Pattern

Components are the primary mechanism for organizing feature modules. Each component extends BaseComponent and registers its repositories, services, and controllers during the binding() lifecycle method.

typescript
export class ApplicationSaleComponent extends BaseComponent {
  constructor(
    @inject({ key: CoreBindings.APPLICATION_INSTANCE })
    protected application: BaseApplication,
  ) {
    super({
      scope: ApplicationSaleComponent.name,
      initDefault: { enable: true, container: application },
      bindings: {},
    });
  }

  override async binding(): Promise<void> {
    this.application.repository(SaleOrderRepository);
    this.application.repository(SaleOrderItemRepository);

    this.application.service(SaleOrderService);
    this.application.service(CheckoutService);

    this.application.controller(SaleOrderController);
  }
}

A component is registered into the application via:

typescript
this.component(ApplicationSaleComponent);

5.1 Component Composition

Components can load other components, creating a composition tree:

6. Controller Pattern

6.1 ControllerFactory (Auto CRUD)

The IGNIS ControllerFactory generates a full set of CRUD endpoints from a single declaration. This is the standard pattern for most controllers in BANA.

typescript
import { controller, inject, ControllerFactory } from '@venizia/ignis';

@controller({ path: '/sale-orders' })
export class SaleOrderController extends ControllerFactory.defineCrudController({
  repository: { name: SaleOrderRepository.name },
  authenticate: { strategies: ['jwt', 'basic'] },
  controller: { name: 'SaleOrderController', basePath: '/sale-orders' },
  entity: () => SaleOrderEntity,
}) {
  constructor(
    @inject({ key: 'repositories.SaleOrderRepository' })
    repository: SaleOrderRepository,
  ) {
    super(repository);
  }
}

The factory generates the following endpoints:

MethodPathDescription
GET/sale-ordersList with filter, pagination, sorting
GET/sale-orders/:idFind by ID
GET/sale-orders/countCount matching filter
POST/sale-ordersCreate
PUT/sale-orders/:idUpdate by ID
DELETE/sale-orders/:idDelete by ID (soft delete)

6.2 Custom Controller Endpoints

Additional endpoints are defined alongside the factory-generated ones by adding methods to the controller class. The IGNIS Filter System is available on all list/count endpoints.

7. Repository and SoftDeletableRepository

7.1 Repository Declaration

Repositories are declared with the @repository decorator, linking a model to a datasource:

typescript
import { repository } from '@venizia/ignis';
import { PostgresCoreDataSource } from '@nx/core';

@repository({ dataSource: PostgresCoreDataSource, model: SaleOrderEntity })
export class SaleOrderRepository extends SoftDeletableRepository<
  TSaleOrderSchema,
  TSaleOrder,
  TSaleOrderPersist
> {}

7.2 SoftDeletableRepository

All BANA repositories extend SoftDeletableRepository instead of DefaultCRUDRepository. This ensures that DELETE operations set a deletedAt timestamp rather than physically removing rows.

MethodBehavior
deleteById(id)Sets deletedAt = NOW() instead of DELETE FROM
restoreById(id)Sets deletedAt = NULL to reverse a soft delete
find(filter)Automatically excludes rows where deletedAt IS NOT NULL

7.3 Database Transactions

Repositories expose the datasource for transaction support:

typescript
await this.repository.dataSource.withTransaction(async (tx) => {
  await this.saleOrderRepository.create({ data: orderData, options: { transaction: tx } });
  await this.saleOrderItemRepository.create({ data: itemData, options: { transaction: tx } });
});

7.4 PostgresCoreDataSource

The shared datasource uses node-postgres with Drizzle ORM. It auto-discovers models from @repository bindings via getSchema():

typescript
@datasource({ driver: 'node-postgres' })
export class PostgresCoreDataSource extends BaseDataSource<IPostgresDataSourceSettings> {
  override configure(): void {
    const schema = this.getSchema(); // Auto-discovers @repository models
    this.pool = new Pool(this.settings);
    this.connector = drizzle({ client: this.pool, schema });
  }
}

All backend packages re-export PostgresCoreDataSource from their local datasources/index.ts, so repositories always reference the same shared datasource.

8. Event-Driven Architecture

BANA uses two complementary event systems for asynchronous communication between services:

MechanismTechnologyUse CaseDelivery
Event BusRedis Pub/SubReal-time notifications between running servicesAt-most-once, fire-and-forget
Job QueuesBullMQ (Redis)Reliable async task processing with retryAt-least-once, with retry and persistence

8.1 Event Flow Diagram

8.2 Event Bus (Redis Pub/Sub)

The RedisPubSubAdapter implements the IEventBus interface defined in @nx/core. It wraps messages in a standardized envelope:

typescript
interface IEventBusMessage<T> {
  id: string;          // Snowflake ID
  type: string;        // Channel name
  publishedAt: string; // ISO timestamp
  data: T;             // Event payload
}

Event Channels defined in @nx/core:

ChannelPublisherConsumerTrigger
payment.order.successSale ServiceFinance, InventoryPayment completed
user.seller.registeredIdentity ServiceCommerce ServiceSeller registration
commerce.initializedCommerce ServiceFinance, InventoryMerchant onboarding complete

8.3 Job Queues (BullMQ)

Each queue domain uses 3 partitions (P01, P02, P03) for load distribution. A hash-based partition selector routes jobs to a specific partition based on a key (typically the entity ID).

Queue Definitions defined in @nx/core:

Queue DomainQueue TypeProducerConsumer
Commerceseller-registeredIdentityCommerce
Commercecommerce-initialized-for-financeCommerceFinance
Commercecommerce-initialized-for-inventoryCommerceInventory
Financepurchase-order-receivedInventoryFinance
Financesale-order-completedSaleFinance
Salepayment-success-for-financeSaleFinance
Salepayment-success-for-inventorySaleInventory
Inventoryproduct_variant_createdCommerceInventory

Queue naming follows the pattern: @nx/{package}/{queue-type}/{partition}

Example: @nx/sale/payment-success-for-finance/01

8.4 WebSocket Events

@nx/core provides utility classes for building WebSocket room and topic identifiers, used by the Signal service:

UtilityFormatExample
WebSocketRooms.build()wr:{prefix}/{paths}wr:observation/merchants/abc-123
WebSocketTopics.build()ws:{paths joined by .}ws:observation.sale.sale-order

9. Bootstrap Helpers

@nx/core provides two bootstrap functions that standardize entry points across all packages. See the IGNIS Bootstrapping reference for the underlying framework concepts.

9.1 Application Bootstrap

Used in each package's src/index.ts to start the HTTP server:

typescript
// packages/sale/src/index.ts
import { bootstrapApplication } from '@nx/core';
import { Application } from './application';
import { appConfig } from './common/app-config';
import { resolve } from 'node:path';

bootstrapApplication({
  ApplicationClass: Application,
  config: appConfig,
  options: { bannerPath: resolve(__dirname, '../resources/banner.txt') },
});

The bootstrap sequence is: new Application() --> init() --> boot() --> start().

9.2 Migration Bootstrap

Used in each package's src/migrate.ts to run database seeds and migrations:

typescript
// packages/sale/src/migrate.ts
import { bootstrapMigration } from '@nx/core';
import { Application } from './application';
import { getMigrationProcesses } from './migrations/processes/migration-process';

bootstrapMigration({
  ApplicationClass: Application,
  getMigrationProcesses,
});

9.3 createMigrationProcessLoader

A factory function that creates a migration process getter from a list of seed file paths:

typescript
// packages/sale/src/migrations/processes/migration-process.ts
import { createMigrationProcessLoader } from '@nx/core';

export const getMigrationProcesses = createMigrationProcessLoader({
  seedPaths: [
    'sale-0001-seed-initial-data',
    'sale-0002-seed-tracking-types',
  ],
  importFn: (path) => import(`../processes/${path}.js`),
});

Each migration process is an object with name, migrateFn, and optional cleanFn. The MigrationHelper class tracks execution status in the database to prevent duplicate runs.

9.4 createAppConfig

Centralizes application configuration so every package uses the same structure:

typescript
// packages/sale/src/common/app-config.ts
import { createAppConfig } from '@nx/core';

export const appConfig = createAppConfig();

The function reads from environment variables and returns an IApplicationConfigs object with host, port, base path, debug settings, and boot options (glob patterns for auto-discovering datasources and repositories from @nx/core).

10. Cross-Service Communication

10.1 IdentityNetworkService

The only cross-service network client in the system. It extends AxiosNetworkRequest from @venizia/ignis-helpers/axios and communicates with the Identity service for credential verification.

typescript
export class IdentityNetworkService extends AxiosNetworkRequest {
  constructor() {
    super({
      name: IdentityNetworkService.name,
      networkOptions: {
        baseUrl: applicationEnvironment.get<string>(
          EnvironmentKeys.APP_ENV_IDENTITY_SERVICE_BASE_URL,
        ),
      },
    });
  }

  async signIn(opts: {
    identifier: { scheme: string; value: string };
    credential: { scheme: string; value: string };
  }) {
    const networkService = this.getNetworkService();
    const response = await networkService.post({
      url: '/auth/sign-in',
      body: opts,
    });
    return response.data;
  }
}

This service is registered by default in DefaultApplication.configureServices(), making it available to all downstream packages for Basic authentication credential verification.

10.2 Communication Topology

There is no API gateway. Each service handles its own authentication and exposes its HTTP API directly. The only inter-service HTTP call is for Basic auth credential verification.

All other cross-service communication happens through Redis Pub/Sub events or BullMQ queues (see Section 8).

11. Authentication Flow

DefaultApplication.configureSecurity() sets up the IGNIS AuthenticateComponent with two strategies.

11.1 JWT Strategy

Stateless token-based authentication. The service validates the token locally without contacting another service.

Environment VariablePurpose
APP_ENV_APPLICATION_SECRETApplication-level secret
APP_ENV_JWT_SECRETJWT signing secret
APP_ENV_JWT_EXPIRES_INToken expiration in seconds

11.2 Basic Strategy

Delegates credential verification to the Identity service via IdentityNetworkService.signIn():

11.3 Strategy Registration

Both strategies are registered in the AuthenticationStrategyRegistry singleton:

typescript
AuthenticationStrategyRegistry.getInstance().register({
  container: this,
  strategies: [
    { name: Authentication.STRATEGY_JWT, strategy: JWTAuthenticationStrategy },
    { name: Authentication.STRATEGY_BASIC, strategy: BasicAuthenticationStrategy },
  ],
});

Controllers specify which strategies to use in their authenticate configuration:

typescript
authenticate: { strategies: ['jwt', 'basic'] }

12. Shared Utilities

@nx/core provides several singleton utilities used across all packages.

UtilityPurpose
IdGeneratorSnowflake ID generation singleton (wraps IGNIS SnowflakeUidHelper)
CryptoUtilityAES-256-GCM encryption and HMAC signing using APP_ENV_APPLICATION_SECRET
useRequestContext()Extracts authenticated user, roles, and provides response formatting helpers
@logged decoratorMethod-level performance measurement logging
RedisConnectionFactoryCreates single-mode or cluster-mode Redis connections

12.1 IdGenerator

typescript
import { IdGenerator } from '@nx/core';

const id = IdGenerator.getInstance().nextId();
// Returns: '7193487234817024' (Snowflake ID string)

Configured via environment variables:

  • APP_ENV_SNOWFLAKE_WORKER_ID (0-1023, unique per service instance)
  • APP_ENV_SNOWFLAKE_EPOCH_CHECKPOINT (custom epoch in milliseconds)

12.2 useRequestContext()

A wrapper around the IGNIS useRequestContext that adds BANA-specific fields:

typescript
const {
  context,        // Hono request context
  currentUser,    // JWT payload with userId, roles
  userId,         // Shortcut: currentUser.userId
  roles,          // Shortcut: currentUser.roles[].identifier
  isAlwaysAllowed, // true if user has SUPER_ADMIN or ADMIN role
  normalizeCountableData,  // Format list responses with count/range headers
  formatResponse,          // Format single-item responses
  formatArrayResponse,     // Format array responses
} = useRequestContext();

13. Package Dependency Chain

All backend packages depend on @nx/core. The following diagram shows the complete dependency tree:

PackageDirect Dependencies
@nx/coreNone (foundation)
@nx/asset@nx/core
@nx/search@nx/core
@nx/inventory@nx/core
@nx/identity@nx/core
@nx/finance@nx/core
@nx/signal@nx/core
@nx/payment@nx/core, @nx/mq-pay
@nx/sale@nx/core
@nx/commerce@nx/core, @nx/asset, @nx/search, @nx/inventory

14. Database Schemas

@nx/core defines all Drizzle ORM schemas centrally so that every service accesses the same database structure. Schemas are organized across 7 PostgreSQL schemas:

PostgreSQL SchemaModel CountDomain
public30Users, roles, products, merchants, organizers, configurations
allocation4Event seating and venue layouts
pricing7Fares, pricing rules, costs, taxes
inventory8Stock, purchase orders, vendors, tracking
finance3Wallets, transactions, categories
payment1Webhook configurations
sale2Sale orders and items

For detailed schema documentation, see Database ERD.

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.