Filling the Unit System: IntegrationUnit, AdapterUnit, and the Full Boot Sequence

Published: (February 25, 2026 at 01:52 PM EST)
13 min read
Source: Dev.to

Source: Dev.to

Filling the Unit System – IntegrationUnit, AdapterUnit and the Full Boot Sequence

Source: Dev.to – Filling the Unit System


Table of Contents

  1. Overview
  2. IntegrationUnit
  3. AdapterUnit
  4. Full Boot Sequence
  5. Practical Example
  6. Key Take‑aways
  7. References

Overview

The Unit System in a modular application provides a clean way to separate concerns, manage dependencies, and orchestrate the start‑up of each component.
Two core building blocks are:

BlockResponsibility
IntegrationUnitRegisters services, repositories, and any other internal dependencies.
AdapterUnitExposes the integration layer to the outside world (e.g., HTTP, gRPC, message‑bus).

Understanding how these units are wired together and how the full boot sequence works is essential for building maintainable, testable applications.


IntegrationUnit

What it does

  • Registers domain services, repositories, use‑cases, and infrastructure components.
  • Keeps the internal graph of dependencies isolated from external adapters.

Typical structure

// Kotlin example – IntegrationUnit.kt
class IntegrationUnit(
    private val config: AppConfig,
    private val db: Database
) {

    // 1️⃣ Repositories
    fun userRepository(): UserRepository = UserRepositoryImpl(db)

    // 2️⃣ Services / Use‑cases
    fun userService(): UserService = UserServiceImpl(userRepository())

    // 3️⃣ Additional infrastructure (e.g., event bus)
    fun eventBus(): EventBus = EventBusImpl()
}

Key points

  • Pure Kotlin/Java – no framework‑specific annotations.
  • All objects are created lazily (or eagerly, depending on the design).
  • The unit can be instantiated in tests with mock dependencies, enabling fast unit testing.

AdapterUnit

What it does

  • Takes the public contract (HTTP routes, gRPC services, CLI commands, etc.) and delegates to the IntegrationUnit.
  • Handles serialization, validation, authentication, and error mapping.

Typical structure

// Kotlin example – HttpAdapterUnit.kt
class HttpAdapterUnit(
    private val integration: IntegrationUnit,
    private val router: Router = Router()
) {

    init {
        // Register routes
        router.get("/users/{id}") { ctx ->
            val id = ctx.pathParam("id")
            val result = integration.userService().getUser(id)
            ctx.json(result)
        }

        router.post("/users") { ctx ->
            val payload = ctx.bodyAsClass(CreateUserRequest::class.java)
            val created = integration.userService().createUser(payload)
            ctx.status(201).json(created)
        }
    }

    fun start(port: Int = 8080) = router.start(port)
}

Key points

  • Thin layer – it should contain no business logic.
  • Keeps the adapter code (e.g., Ktor, Spring MVC, Express) separate from the core logic.
  • Makes it trivial to swap adapters (e.g., replace HTTP with gRPC) without touching the integration layer.

Full Boot Sequence

The boot process stitches the two units together and starts the application.
A typical sequence looks like this:

  1. Load configuration (environment variables, config files, etc.).
  2. Create infrastructure resources (database pool, message‑bus client, etc.).
  3. Instantiate IntegrationUnit with the resources from step 2.
  4. Instantiate AdapterUnit passing the IntegrationUnit.
  5. Start the adapter (listen on a port, subscribe to a queue, etc.).

Example boot file

// Kotlin – Main.kt
fun main() {
    // 1️⃣ Load config
    val config = AppConfig.load()

    // 2️⃣ Initialise infrastructure
    val db = Database.connect(config.dbUrl, config.dbUser, config.dbPassword)

    // 3️⃣ Build IntegrationUnit
    val integration = IntegrationUnit(config, db)

    // 4️⃣ Build AdapterUnit
    val httpAdapter = HttpAdapterUnit(integration)

    // 5️⃣ Start the server
    httpAdapter.start(port = config.httpPort)

    // Optional: add graceful shutdown hook
    Runtime.getRuntime().addShutdownHook(Thread {
        db.close()
        httpAdapter.stop()
    })
}

Why this order matters

  • Deterministic startup – each component receives fully‑initialised dependencies.
  • Testability – you can replace any step with a mock (e.g., an in‑memory DB).
  • Graceful shutdown – resources are released in reverse order, preventing leaks.

Practical Example

Below is a minimal, end‑to‑end example that you can copy‑paste into a Kotlin project (using Ktor as the HTTP engine).

// ---------- AppConfig.kt ----------
data class AppConfig(
    val dbUrl: String,
    val dbUser: String,
    val dbPassword: String,
    val httpPort: Int
) {
    companion object {
        fun load(): AppConfig = AppConfig(
            dbUrl = System.getenv("DB_URL") ?: "jdbc:h2:mem:test",
            dbUser = System.getenv("DB_USER") ?: "sa",
            dbPassword = System.getenv("DB_PASSWORD") ?: "",
            httpPort = System.getenv("HTTP_PORT")?.toIntOrNull() ?: 8080
        )
    }
}

// ---------- IntegrationUnit.kt ----------
class IntegrationUnit(
    private val config: AppConfig,
    private val db: Database
) {
    fun userRepository(): UserRepository = UserRepositoryImpl(db)
    fun userService(): UserService = UserServiceImpl(userRepository())
}

// ---------- HttpAdapterUnit.kt ----------
class HttpAdapterUnit(
    private val integration: IntegrationUnit,
    private val engine: ApplicationEngine = embeddedServer(Netty, port = integration.config.httpPort)
) {
    init {
        engine.routing {
            get("/users/{id}") {
                val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest)
                val user = integration.userService().getUser(id)
                call.respond(user)
            }

            post("/users") {
                val request = call.receive<CreateUserRequest>()
                val created = integration.userService().createUser(request)
                call.respond(HttpStatusCode.Created, created)
            }
        }
    }

    fun start() = engine.start(wait = true)
    fun stop() = engine.stop(gracePeriodMillis = 1000, timeoutMillis = 5000)
}

// ---------- Main.kt ----------
fun main() {
    val config = AppConfig.load()
    val db = Database.connect(config.dbUrl, config.dbUser, config.dbPassword)

    val integration = IntegrationUnit(config, db)
    val httpAdapter = HttpAdapterUnit(integration)

    Runtime.getRuntime().addShutdownHook(Thread {
        db.close()
        httpAdapter.stop()
    })

    httpAdapter.start()
}

What you get

FeatureImplementation
ConfigurationAppConfig reads from env vars (fallback defaults).
DatabaseSimple Database wrapper (could be Exposed, JOOQ, etc.).
Integration layerIntegrationUnit wires repository → service.
Adapter layerHttpAdapterUnit uses Ktor routes, delegates to the service.
Boot & shutdownmain() orchestrates everything and adds a graceful shutdown hook.

Key Take‑aways

Take‑away
1️⃣Separate concerns – keep business logic (IntegrationUnit) away from transport logic (AdapterUnit).
2️⃣Explicit wiring – the boot sequence should clearly show what is created when and why.
3️⃣Testability – each unit can be instantiated with mocks, enabling fast unit and integration tests.
4️⃣Swap‑ability – you can replace the HTTP adapter with a gRPC or CLI adapter without touching the core.
5️⃣Graceful shutdown – always release resources in reverse order of creation.

References


Feel free to adapt the snippets to your preferred language or framework; the core ideas—clear separation, explicit boot order, and testability—remain the same.

Last post

The Unit System introduced its first real types — RuntimeUnit, PackageManager, and defineRuntime(). The runtime layer is complete, but a runtime by itself does nothing: it knows how to run things, yet nothing is asking it to run anything. The system still needs the other two unit kinds — integrations and adapters — before it can actually connect.


IntegrationUnit

IntegrationUnit is the interface for framework‑specific plugins — React, NestJS, Angular, Docker (as an orchestrator), and anything else that wires into the Velnora lifecycle.

If runtimes are the foundation and adapters are the build tools, integrations are the pieces that actually give a workspace its identity.

Design highlights

  • All lifecycle hooks are optional

    • An integration implements only what it needs.
    • If it only needs to expose an API during initialization, it implements configure().
    • If it also needs to run something at build time, it adds build().
    • If it needs neither, it can still exist as a unit that merely declares capabilities for others to depend on.
  • This is Interface Segregation in practice.

    • The kernel duck‑types before calling (e.g., 'configure' in unit).
    • Consequently, there is no dead code, no empty method stubs, and no forced implementations.

Two Phases, Two Contexts

Every unit kind now gets phased contexts.

HookContext typeCapabilities
configure()IntegrationConfigureContextFull access to ctx.expose() and ctx.query()
build()IntegrationBuildContextQuery‑only (registration is closed, no new APIs can be exposed)

Right now all three unit kinds share the same idea, but the context types are not fully separated yet. Runtimes, adapters, and integrations each have different phase‑specific needs, and the concrete context interfaces still need to be split out per kind. That is a task for the next round.

The separation is not just for safety. It makes the intent clear at the type level. If you are inside build(), TypeScript will not even let you call expose(). You know exactly what phase you are in by looking at the context type.

defineIntegration

Just like defineRuntime(), the factory is thin:

defineIntegration(integrationDefinition)
  • It injects kind: 'integration' and returns a VelnoraUnit.
  • The same pattern, same type inference, same generics capturing literal strings from requires and capabilities.

Calling conventions

All three define helpers — defineRuntime(), defineAdapter(), and defineIntegration() — support a second calling convention: a factory function.

defineIntegration((env: ConfigEnv) => ({
  // …config that can depend on env.command, env.mode, etc.
}))
  • Use this when you need to change behavior based on whether you are running dev or build.
  • The factory wraps your function and injects kind at call‑time.

Both conventions produce the same result: a static definition for simple cases, or a factory for environment‑aware ones. The pattern is identical across all three unit kinds.

Init Order

In theory, integrations should initialize last—after runtimes and adapters. By the time an integration runs its configure() hook, every runtime is booted and every adapter is ready, so the integration can safely query anything it depends on.

Ordering within the integration tier

  • A topological sort based on requires and optionalRequires determines the exact order.
  • Example: if your integration depends on 'docker', the Docker integration will be configured first.

Note: The resolver that performs this sorting is not wired up yet.

Open question: circular dependencies

What happens when A requires B and B requires A?

  • The strategy is still undecided.
  • Possible approaches:
    1. Reject the cycle at startup.
    2. Break the cycle with a warning and continue.
  • For now the design assumes no cycles—creating one will lead to undefined behavior. A concrete policy must be defined before the resolver ships.

AdapterUnit — The Middle Layer

After the integrations are in place, the next step is the adapter. AdapterUnit represents build‑tool orchestrators such as Vite, Webpack, Turbopack, esbuild, and similar.

Required hooks

dev(project: Project, ctx: AdapterDevContext): DevServerResult
build(project: Project, ctx: AdapterBuildContext): BuildResult
  • Both hooks are required.
  • dev() returns a DevServerResult – connection information that the Host uses to wire up the dev server.

Design shift: removing mode

Earlier iterations exposed a mode: 'thread' | 'process' property on the adapter. That was a mistake.
Whether something runs in‑thread or as a child process is the runtime’s decision, not the adapter’s.

  • A Vite adapter should not care if it runs inside Node’s process or is spawned separately.
  • It only declares what to run.
  • The runtime decides how to run it.

Execution flow

  1. The adapter queries the runtime via ctx.query().
  2. It calls something like execute().
  3. The runtime returns a declarative ExecutionPlan (a discriminated union).
ModeWhat the ExecutionPlan contains
ThreadA run function that Velnora invokes in‑process.
ProcessThe binary, arguments, and cwd for Velnora to spawn as a child process.

Either way, the adapter code stays unchanged, and the same adapter works with Node, Bun, or any other runtime.

Phased contexts

  • AdapterDevContext – passed to dev().
  • AdapterBuildContext – passed to build().

Defining an adapter

defineAdapter() follows the same pattern as the other helpers:

  • Accepts either a static object or a factory function.
  • The kind field is injected automatically.
  • Full type inference is provided for dependencies.

The Full Boot Sequence (In Theory)

Note: The init order described below is not implemented yet. It outlines the intended flow once the resolver is wired up.

  1. Runtimes boot first.
  2. Adapters initialize next (making dev/build hooks available).
  3. Integrations configure last, after all runtimes and adapters are ready.

Within each tier, a topological sort based on requires / optionalRequires resolves the exact ordering.

When the resolver is in place, the system will:

  • Enforce this sequence automatically.
  • Detect dependency cycles.
  • Provide clear error messages.

How It Should Work

Runtimes – language runtimes boot first
Adapters – build tools next
Integrations – frameworks last

Within each tier, a topological sort by requires and optionalRequires determines the exact ordering. You drop units into one array and the Kernel figures out the rest—that is the design. What actually happens in practice is a different story, but this is the contract the system is built around.

All three unit kinds are now implemented:

LayerUnits / APIs
RuntimeRuntimeUnit, PackageManager, Toolchain, defineRuntime()
AdapterAdapterUnit, the execution‑model split, defineAdapter()
IntegrationIntegrationUnit, phased contexts, defineIntegration()

The Unit System is complete—every unit kind has its interface, its factory, and its place in the boot sequence.


A Note on AI and How I Work

I use AI for generating tests and JSDoc, and in rare cases for code. Even when it generates code, I review it multiple times before applying it to the project and committing. The code architecture is mine.

The way the work gets seen—which packages should exist, how the spec should read, how the design should be documented—is where I use Notion AI. Each time I work on a feature, the process takes almost a couple of hours:

  1. Understand the design.
  2. Consult with AI.
  3. Analyze the AI output again.
  4. Document it properly.

The thinking is mine; the documentation process is a collaboration.

Current limitation:
Almost every code assistant has usage caps—except Notion AI, which I have already exhausted. Consequently, tests and JSDoc generation are temporarily unavailable until I get a local assistant running. I’m setting one up, but it isn’t fully ready yet. Right now I’m creating a connection between my PC and my Mac so I can run it remotely, independent of distance.


What Is Next

The Unit System is complete—every unit kind has its interface, its factory, and its place in the boot sequence. The next step is wiring them into the Kernel’s actual resolver and creating the first real unit packages. This means:

  • Real defineRuntime() calls for Node and Bun.
  • A real defineAdapter() for Vite.
  • A real defineIntegration() for React.

Interfaces are done; now it’s time to fill them.

0 views
Back to Blog

Related posts

Read more »

Dragon Ball Color Correction Process [pdf]

I’m sorry, but the content you provided is a PDF file in binary format, not HTML or plain text. I’m unable to extract and convert the article’s text from this b...