← Back to blog

Building a Firecracker VM Orchestrator in Go — Part 1: Provider Interfaces

Introduction

I’m building Flames, an open-source control plane for managing microVMs powered by Firecracker and Jailer. The goal is a straightforward API to spin up, orchestrate, and tear down lightweight VMs — the kind of ephemeral, hardware-isolated environments that are becoming critical infrastructure. Especially in the AI ecosystem, where you’re running untrusted code, agent workflows, or sandboxed execution, container-level isolation isn’t enough. You need real VM boundaries with Jailer-enforced security, and you need it to be fast and programmable. That’s what Flames is for.

I’ve been coding with AI agents for a while now, but what’s different this time is that I’m using ContextPin as my main AI coding workspace — organizing specs, context, and decisions in one place so the AI always has what it needs. Spec-Driven Development, essentially.

Requirements

Design

Tasks

I’m documenting the whole journey here.

The idea is simple: I write the specs, I bring my Go experience to the table, and I let AI handle the bulk of the implementation. This frees me up to spend more time on architecture, code review, and making sure the design decisions are sound — thinking about interfaces, data flow, concurrency trade-offs — while moving through the implementation phase much faster than I could solo.

This first spec tackles the most foundational piece: the provider interfaces. Five abstractions that decouple the entire control plane from any specific infrastructure backend.

The Problem

Flames needs to store VM state, cache data, queue background jobs, store blobs, and expose VMs via ingress. But I’ve been burned before by coupling to a specific database or queue system too early. A developer running go run locally shouldn’t need a database cluster. A production deployment should plug in real backends without changing application code.

If you’ve written Go for any amount of time, you know the answer: narrow interfaces with in-memory defaults. The interesting part is getting the contracts right on the first pass — and that’s where the spec-driven approach really pays off.

API Server

StateStore

BlobStore

IngressProvider

Scheduler

WorkQueue

Reconciler

CacheStore

VM

Controller

Event

BlobMeta

Job

Endpoint

The green nodes are the five provider interfaces — the abstraction boundary. The dark nodes are the domain models each interface operates on. The control-plane components at the top consume only interfaces, never concrete implementations.

The Spec

Before writing a single line of Go, I wrote three structured notes in ContextPin:

  • Requirements — 7 user stories, 30+ functional requirements, acceptance criteria, and non-functional constraints. This is the contract.
  • Design — Package layout, dependency graph, data models, interface signatures, implementation notes, and risk analysis. This is the blueprint.
  • Tasks — The ordered implementation checklist derived from the design.

Having these written down before implementation meant I could hand them to Claude Code and say “build this” — and then spend my time reviewing the output against my own spec rather than dictating every line. The spec becomes the shared language between me and the AI.

The Five Interfaces

  • StateStore (provider/state) — VM, controller, and event records. Default: memstate (maps + mutex)
  • BlobStore (provider/blob) — Opaque artifact storage. Default: memblob (byte slices)
  • CacheStore (provider/cache) — Ephemeral key-value caching. Default: memcache (map + TTL)
  • WorkQueue (provider/queue) — Background job processing. Default: memqueue (slices + leases)
  • IngressProvider (provider/ingress) — VM service exposure. Default: noop (no network ops)

Each interface lives in its own package, imports only from model/ and provider/providererr/, and has zero external dependencies in its default implementation. This is the kind of structure I’d set up in any Go project — clean import graphs, no circular dependencies, everything testable in isolation.

Key Design Decisions

These are the calls I made during the spec phase — the kind of decisions that are hard to delegate to AI because they require judgment about where the project is heading.

Interface-per-package over monolithic provider

One interface per package rather than a single Provider mega-interface. I’ve seen the mega-interface approach in other projects and it always ends the same way: a component that only needs blob storage ends up importing the entire provider dependency tree. Keeping them separate also maps cleanly to future adapter packages — provider/state/postgres imports provider/state and nothing else.

Non-blocking Dequeue

WorkQueue.Dequeue returns ErrNoJobs instead of blocking. I went back and forth on this one, but non-blocking is simpler to implement correctly across all backends. The callers (reconciler, scheduler) will already run on tick-based loops anyway. If we ever need a push model, that’s a separate optional interface — not a change to the core contract.

Conformance test suites

This is the one I’m most excited about. Every interface has a shared test suite in providertest/ that any adapter can import and run. The in-memory default passes it today. A future Postgres adapter runs the exact same tests. This is what turns interfaces from “type signatures that might work” into actual enforceable contracts. During review, I spent most of my time here — making sure the test cases cover the edge cases that matter.

Structured errors with errors.Is support

Provider errors carry metadata (resource type, ID) and support errors.Is matching against sentinels like ErrNotFound, ErrConflict, ErrCacheMiss. No string matching, ever. This is a pattern I’ve used in every serious Go project — the upfront cost is minimal and it saves hours of debugging later.

Reflections on the Workflow

I didn’t write these specs by hand, line by line. I brought my Go and infra expertise — I’d already done a Firecracker + Jailer proof of concept internally — and explained exactly how I wanted the architecture to work. The AI helped me turn that into structured requirements, design docs, and task breakdowns. It’s a conversation, not dictation.

But once the specs were done and living in ContextPin, everything moved fast. The AI produced code that matched my design because the design was explicit, not in my head. My review cycles were focused: does this match the spec? Does it handle the edge cases? Are the error types right?

I spent almost no time on implementation details and almost all my time on the things that matter for long-term quality: interface design, concurrency safety, test coverage.

What’s Next

With the provider interfaces in place, the next spec will build on top of them — likely the API server or scheduler, which consume these interfaces via constructor injection. The in-memory defaults mean I can develop and test the next layer without standing up any infrastructure. That’s the whole point.


This post is part of the Flames Spec-Driven Development series.


This entire project is being built using ContextPin + Claude Code. ContextPin is an ADE (Agentic Development Environment) — a GPU-accelerated desktop app designed for AI-assisted development that integrates directly with Claude Code, Codex, Gemini and OpenCode. ContextPin is going open-source soon. If you want to get early access, you can join here.

Strand
Written by Strand

Full-Stack Software Engineer with 13+ years of experience. Writing about engineering, AI-assisted coding, and the craft of building software.