title: 'Lazy-loading modules · Nael Framework' description: 'Load feature modules on demand with LazyModuleLoader and the ApplicationContext hooks.'

Fundamentals

Lazy-loading modules

Skip eager initialization for heavy features and load them only when a tenant, route, or job actually needs them. Nael exposes a LazyModuleLoader service plus new ApplicationContext hooks so platforms can register modules at runtime without rebooting the app.

When to lazy load

Reach for lazy loading when a feature pulls in expensive providers (ORM connections, external SDKs, giant resolvers) that only a subset of users require. Instead of bloating startup time, keep those modules on the shelf and register them just-in-time once you have enough context to know they are needed.

  • Multi-tenant SaaS: initialize tenant-specific adapters when the first request for that tenant lands.
  • Admin-only workflows: keep reporting or billing modules cold until an administrator invokes them.
  • Background workers: hydrate specialized modules inside scheduled jobs without impacting the main HTTP bootstrap.

When to use LazyModuleLoader

Imagine a SaaS platform where only premium tenants can access the reporting suite. Instead of registering every reporting adapter and data warehouse client on startup, keep the module cold and let the first premium request trigger the load. Subsequent requests from that tenant get the warmed module instantly, while standard tenants never pay the cost.

Tie the loader into your tenant resolution logic (guards, middleware, schedulers) and call loader.load(PremiumReportsModule) only when tenant.plan === 'premium'. This keeps memory predictable and shortens boot time for the common case without removing capabilities for customers who need them.

Loading modules with LazyModuleLoader

The LazyModuleLoader is injectable from any application context (core, HTTP, GraphQL, schedulers). Call load() with either a module class or a factory that performs a dynamic import. The loader registers the module, instantiates its providers, and emits a load event so other parts of the system can react.

Load a module on the first HTTP hit
import { LazyModuleLoader } from '@nl-framework/core';import { createHttpApplication } from '@nl-framework/http';    const http = await createHttpApplication(AppModule, { port: 0 });    const loader = await http.get(LazyModuleLoader);    await loader.load(async () => (await import('./reports/reports.module')).ReportsModule);

The loader understands default exports as well, so returning the raw import() promise works if your module file exports a default class.

Reacting to module load events

The ApplicationContext now exposes loadModule() and addModuleLoadListener(). Use them to wire up your own bookkeeping (analytics, cache warmers, telemetry) whenever new modules join the container.

Track loaded modules
import { Application } from '@nl-framework/core';    const app = new Application();    const context = await app.bootstrap(AppModule);    const unsubscribe = context.addModuleLoadListener(({ module, controllers }) => {console.log('Loaded module:', module.name, 'controllers:', controllers.length);  });    await context.loadModule(ReportsModule);unsubscribe();

Listeners return an unsubscribe function. They run after the module's providers, controllers, and bootstrap tokens are fully instantiated.

Platform behavior

Built-in platforms already listen for module load events so routes, resolvers, and guards stay in sync:

  • HTTP: new controllers are registered with the router immediately—no need to restart the server. Custom middleware can lazily grab services from the same context ID.

  • GraphQL: when a module adds resolvers, the schema is invalidated and rebuilt on the next request so clients see the new queries and mutations without downtime.

Load GraphQL modules on demand
import { LazyModuleLoader } from '@nl-framework/core';import { GraphqlApplication } from '@nl-framework/graphql';    const graphql = await createGraphqlApplication(AppModule);    const loader = await graphql.get(LazyModuleLoader);    await loader.load(() => import('./billing/billing.module'));// Next request sees the rebuilt schema automatically.

Operational tips

  • Lazy modules still respect request-scoped providers. The loader uses the active context ID of the caller when instantiating services.
  • Avoid circular lazy loads; rely on forwardRef() or split shared providers into a third module if two lazy modules depend on each other.
  • Keep an eye on memory. Once loaded, modules remain registered for the lifetime of the process.