Architecture
Overview
AntelopeJS extends the ports-and-adapters model across application and ecosystem modules. Interfaces define capabilities, provider modules implement them, and application configuration selects the modules to load. The core manages their bindings and lifecycle.
| Hexagonal concept | AntelopeJS mechanism | Responsibility |
|---|---|---|
| Port | Interface contract | Describe the public capability consumers need. |
| Adapter | Implementation in a provider module | Translate that capability into provider-specific behavior. |
| Consumer | Module calling an interface | Depend on the capability rather than its implementation. |
| Composition root | Application configuration and assembly | Select and configure the implementations. |
A module is a unit of composition, not necessarily an adapter. It can contain business behavior, implement contracts, consume other contracts, or combine these roles. You still choose the boundaries that suit your application.
Interfaces — Contracts Between Modules
An interface defines a public contract: functions, events or registrations that consumers and implementations agree on. It can also contain shared types, wrappers and provider-independent utilities. It is more than a TypeScript type declaration.
A standalone interface package has its own release cycle. A consumer imports that package, while a provider imports it to register an implementation. Embedded interfaces also exist; see Interfaces for both forms.
The arrows below show dependencies on a shared contract, not the runtime call path:
Consumer ──depends on──▶ Contract ◀──implements── Provider
At runtime, the consumer calls the interface API, and its proxies route calls to the registered implementation. The consumer does not need to import the provider package, but it still depends on the contract's API and behavior.
Application Assembly — Choose the Implementation
The application selects module sources in antelope.config.ts. Sources can be local modules, packages or Git repositories. A provider's construct() function registers its implementation with ImplementInterface; the consuming module does not make that choice.
This separation also applies when the provider comes from the ecosystem. You can keep a critical integration in-house without changing the way the rest of the application consumes its contract. See Configuration for source types and module settings.
An incoming adapter, such as an HTTP module, connects requests to application behavior. A business module can then call outgoing capabilities through contracts. Keep the dependency diagram separate from this request flow: an incoming adapter calling a use case does not require the use case to depend on that adapter.
Modules — Building Blocks
Modules declare the interfaces they provide or consume and have their own dependencies, configuration and lifecycle. Their boundaries support separate development and testing; they do not automatically imply separate deployment or process isolation.
Every module follows the same lifecycle:
- Construct — The module initializes its internal state
- Start — The module begins serving its interfaces
- Stop — The module stops serving and cleans up active work
- Destroy — The module releases all resources
Each lifecycle function maps to a specific state transition:
| Function | Transition |
|---|---|
construct() | loaded → constructed |
start() | constructed → active |
stop() | active → constructed |
destroy() | constructed → loaded |
Replacing a Provider — Check Compatibility
A consumer can keep the same import and calls when a replacement satisfies the contract version, capabilities and behavior it relies on. Selecting a different source moves the provider choice; it does not migrate data or establish behavioral equivalence.
For example, the ecosystem's MongoDB and RethinkDB modules both declare the shared database interface, alongside provider-specific interfaces. A consumer using the shared API has a common boundary to evaluate. A consumer using MongoDB-specific operations has deliberately taken on a provider dependency.
Before replacing a provider, check:
- Contract versions — The replacement supports the interface versions your consumers use.
- Capabilities and behavior — Required operations, error handling and side effects match your application's expectations.
- Configuration and data — Credentials, provider settings and any migration are ready.
- Operational requirements — Performance, durability and deployment characteristics suit the application.
- Tests — Run available contract suites and your integration tests against the replacement. Passing tests provides evidence for the behaviors they cover, not a guarantee of universal interchangeability.
AntelopeJS supplies binding and lifecycle mechanisms, not enforcement of dependency direction. Direct provider imports can still bypass the contract boundary. Development HMR is also separate from a production provider migration.
The Core — The Conductor
The core is minimal by design. It handles three responsibilities:
- Module lifecycle — Loading, starting, stopping, and destroying modules in the correct order
- Interface routing — Connecting interface producers to consumers automatically
- Runtime management — Graceful shutdown, Hot Module Reloading (HMR) orchestration, and error propagation
The core does not contain business logic. Instead, the core acts as the conductor: it makes sure modules start in the right order, interfaces find their implementations, and failures propagate cleanly.
Benefits
The benefit is a shared integration model, not the absence of dependencies:
- Provider-independent consumers — Keep implementation-specific imports out of modules that only need the shared contract.
- Local and ecosystem composition — Apply the same assembly mechanisms to modules you write and modules you install.
- Focused changes — Evolve an implementation behind a compatible contract without spreading provider choices through its consumers.
- Less runtime plumbing — Use the core's interface binding and lifecycle instead of rebuilding them for each application.
Error Handling
Each lifecycle phase propagates errors differently.
construct() — Errors propagate to the module manager. The resolver detour detaches on failure. Modules construct in parallel via Promise.all, so other modules may continue constructing, but the first rejection causes the overall boot to fail.
start() — This function is synchronous with no internal try/catch. An exception propagates immediately and prevents remaining modules from starting. The system may end up in a mixed state where some modules have started and others have not.
stop() / destroy() — Errors propagate similarly. These functions can be async or sync. A failure during teardown does not automatically roll back other modules that have already stopped or been destroyed.
See also
- Modules — Module states and lifecycle details
- Creating a Module — Practical guide to implementing lifecycle functions