Architecture
Titan is an orchestrator over nine focused subsystems. Each subsystem has one responsibility, one entry point, and a typed contract with the others. Understanding the boundaries makes the framework significantly easier to reason about.
The nine subsystems
Each block in the diagram is a real type with a documented public
interface. None of them know about each other except through the
contracts declared in src/types/.
Subsystem responsibilities
Bootstrap (Application.create, .start, .stop)
The bootstrap layer is the single entry point. It does three things and nothing else:
- Builds the Container (see DI below).
- Resolves and instantiates the Module Registry (see Modules below).
- Hands control to the Lifecycle state machine.
After bootstrap completes, the Application object is a façade — it
holds references to the other subsystems and exposes their capabilities
through a small public surface (use, resolve, on, emit,
config).
→ Reference: Application Bootstrap
Lifecycle state machine
Drives transitions between states (created → starting → started → stopping → stopped) and fires hooks in dependency order at each
transition. Owns timeouts, parallelism rules, and the hard-exit
guarantee.
States:
| State | Meaning |
|---|---|
created | Container built, no hooks fired yet |
initializing | onInit hooks running |
initialized | All onInit complete; ready to start |
starting | onStart hooks running |
running | onStart complete; service surfaces are live |
stopping | onStop hooks running (reverse dependency order) |
stopped | onStop complete |
shuttingDown | onShutdown hooks running; phased timeouts active |
error | A hook failed; the application is in a poisoned state |
→ Reference: Lifecycle, Shutdown
Event Bus
Broadcasts framework events (module:registered, config:changed,
lifecycle:phase, health:changed, etc.) to subscribers registered via
app.on(event, handler). Strictly typed event names; payload type
inferred from the event.
→ Reference: Application Events
Module Registry
Knows about every module loaded into the application. Computes the dependency graph between modules. Detects circular imports. Drives ordered initialisation.
A module is a static or dynamic descriptor:
@Module({
imports: [LoggerModule, ConfigModule],
providers: [UsersService, UsersRepository],
exports: [UsersService],
})
export class UsersModule {}
→ Reference: Modules, Dynamic Modules
Config Store
A typed, layered, hot-reloadable configuration source. Multiple sources are merged with deep-merge semantics; the last source wins per key. Validators are applied per source or globally.
→ Reference: Configuration
Health Aggregator
Aggregates health probes from registered indicators (database, redis,
disk space, custom). Surfaces a single IHealthStatus (healthy | degraded | unhealthy) plus per-indicator detail. Used by /healthz,
load balancers, and the Omnitron orchestrator.
→ Reference: Health
Shutdown Coordinator
Phased graceful shutdown. Tasks declare a phase (PreShutdown | Cleanup | Flush | Final), a priority, a timeout, and a critical flag.
The coordinator runs each phase to completion (or timeout), then
proceeds to the next. Hard exit after the last phase.
→ Reference: Shutdown
Process Host
Binds OS signals (SIGTERM, SIGINT, SIGHUP), sets up uncaught
exception / unhandled rejection handlers, and exposes runtime metrics
(uptime, memoryUsage, cpuUsage, pid). Disable with
disableGracefulShutdown: true if you embed Titan in a larger process.
Service Exposer
The bridge between the DI container and Netron. When a @Service-marked
provider is resolved, the Service Exposer registers it with the running
Netron LocalPeer so its @Public methods become callable over the
wire.
→ Reference: Netron Services
The two RPC layers
Netron has two layers of middleware that are easy to confuse:
- DI middleware — wraps container resolution. Runs at construction time. Used for cross-cutting object-graph concerns (logging instantiation, retry on construction failure, caching resolved instances).
- RPC middleware — wraps Netron calls. Runs per-call. Used for cross-cutting wire concerns (auth, rate limiting, tracing, serialisation tweaks).
They are independent. A typical app uses neither, one, or both depending on what it needs.
→ Reference: DI Middleware, RPC Middleware
What lives where
| Concern | Subpath |
|---|---|
| Application + lifecycle | @omnitron-dev/titan/application |
| Lifecycle interfaces | @omnitron-dev/titan/lifecycle |
| DI container (Nexus) | @omnitron-dev/titan/nexus |
| Decorators | @omnitron-dev/titan/decorators |
| Validation | @omnitron-dev/titan/validation |
| Errors | @omnitron-dev/titan/errors |
| Netron RPC | @omnitron-dev/titan/netron |
| Transports (subpaths under netron) | …/netron/transport/{http,websocket,tcp,unix} |
| Auth (subpath under netron) | …/netron/auth |
| Multi-backend | …/netron/multi-backend |
| Config module | @omnitron-dev/titan/module/config |
| Logger module | @omnitron-dev/titan/module/logger |
| Tracing | @omnitron-dev/titan/tracing |
| Resilience helpers | @omnitron-dev/titan/utils |
These subpaths are stable. Importing from @omnitron-dev/titan (no
subpath) re-exports the most common surfaces; subpath imports keep
your bundle tighter.
Boundaries between subsystems
The boundaries are not just architectural diagrams — they are concrete rules the framework enforces:
- The Application kernel never owns business state. It owns metadata about modules, providers, and lifecycle. Business state lives in your services, owned by the container.
- The container never knows about Netron. Services that happen to
be
@Service-marked are also registered with Netron, but the container treats them as ordinary providers. - Netron never knows about your business logic. It dispatches to methods. The methods are written in plain TypeScript; they are not aware they are being called over the wire.
- Modules never directly call each other. Modules import and export providers. Providers are what gets injected. A module instance is a passive descriptor, not an addressable object.
These boundaries are why Titan tests cleanly: you can replace any subsystem in isolation. You can mock the container. You can boot without Netron. You can bind a fake transport for end-to-end tests without sockets.
→ Next: Mental Model — how to think about a running Titan app.