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.
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.
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.
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.