Sitemap
MonoBit

MonoBit

Follow @TonScholar on Telegram

Building a Lightweight DI for Hono🔥: Our Circulo Story

3 min readDec 4, 2025

--

Press enter or click to view image in full size

I’ve spent the last stretch at Circulo taking our tiny, explicit dependency-injection experiment and shaping it into a publishable package: @circulo-ai/di. This is the story of why we built it, what trade-offs we made, and how it compares to other Hono-friendly DI options.

we had a messy, evolving Hono app and needed per-request scoping, multi-implementations, and deterministic disposal without decorator magic x

The Problem We Hit

Our Hono server grew fast — auth, AI, file uploads, queues. We needed:

- Scopes per request to avoid global leaks.

- Multi/Keyed services (think multiple caches, repos, or handlers for the same abstraction).

- Deterministic disposal for DB/queue clients.

- No decorator/reflect gymnastics so bundlers stayed happy and wiring stayed explicit.

Get MonoBit’s stories in your inbox

Join Medium for free to get updates from this writer.

Off-the-shelf DI for Hono felt either too magical or too minimal, so we wrote our own.

What We Built

  • Lifetimes: singleton, scoped, transient.
  • Multi & keyed registrations: `addXXX(token, factory, { multiple: true, key })`; `resolveAll` or `resolve(token, key)`.
  • Optional resolution: `tryResolve`, `tryResolveFromContext`.
  • Per-request scopes: `createContainerMiddleware` attaches a scope; we dispose it after each request and call `dispose/close/destroy` on services when present.
  • No decorators/reflect: explicit tokens and factories keep it bundler-friendly and predictable.
  • Tests + coverage: 100% coverage;

How It Works in Hono

import {
ServiceCollection,
createContainerMiddleware,
resolveFromContext,
tryResolveFromContext,
} from "@circulo-ai/di";

const services = new ServiceCollection()
.addSingleton("Config", { port: 3000 })
.addScoped("RequestId", () => crypto.randomUUID())
.addTransient("Clock", () => () => new Date())
.addSingleton("Cache", () => primaryCache, { multiple: true, key: "primary" })
.addSingleton("Cache", () => secondaryCache, { multiple: true, key: "secondary" });

const provider = services.build();
app.use("*", createContainerMiddleware(provider));

app.get("/ping", (c) => {
const requestId = resolveFromContext<string>(c, "RequestId");
const caches = resolveFromContext<any[]>(c, "Cache", undefined) as unknown as any[];
const optional = tryResolveFromContext<string>(c, "Maybe");
return c.json({ ok: true, requestId, caches: caches.length, optional });
});

How It Compares (Hono DI Landscape)

  • hono-simple-DI (github.com/maou-shonen/hono-simple-DI): very small and decorator-free, but it’s more of a tiny service locator. It doesn’t cover keyed/multi registrations, resolveAll, or disposal semantics. Great for quick demos; we needed richer scoping and cleanup.
  • @hono/tsyringe: wraps tsyringe, which leans on decorators and reflect-metadata. That buys auto-injection, but requires decorator emit and metadata runtime, which we chose to avoid for bundling simplicity and explicitness. Also, keyed/multi patterns and disposal control are thinner in that stack.
  • @circulo-ai/di: explicit tokens, no metadata, scoped middleware, keyed/multi support, disposal, optional resolve and tailored to Hono apps.

What Made It Challenging

  • Scoping + disposal: Ensuring scoped instances clean up while singletons persist required descriptor-based caches (not just token maps) and deterministic disposal ordering.
  • Multi registrations: Picking the right default (`last wins` when no key) while still supporting `resolveAll` and keys without reflection.
  • Zero magic: Without decorators, we had to keep the API concise enough that explicit wiring wasn’t painful.

Hard-Earned Tips

  • Use symbols for tokens when possible to avoid collisions.
  • Default to scoped for request-bound resources (db transactions, loggers), singleton for configs/clients, transient for short-lived helpers.
  • Always expose `dispose/close/destroy` on anything holding a handle; we call them automatically when scopes/providers dispose.
  • Keep DI wiring at edges (bootstrap, middleware) so handlers stay lean.

Why We’re Happy With It

It’s explicit, fast, and predictable. It doesn’t force decorators or metadata on your toolchain, and it fits Hono’s minimal ethos while giving us scoped lifetimes, keyed/multi registration, and deterministic cleanup — exactly what our Circulo services needed.

Join the Circulo Waitlist

If this DI story resonates — especially the “explicit, scoped, no-magic” approach — you’ll probably vibe with what we’re building at Circulo more broadly. We’re putting the same philosophy into our product: practical AI systems that fit cleanly into real services without turning your stack into a science project.

We’re opening access in waves. If you want early entry, updates, and a chance to help shape what ships next, hop on the waitlist here.

Would love to have you along for the ride.

--

--

Written by MonoBit

Follow @TonScholar on Telegram