<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Strand</title>
    <description>The latest articles on DEV Community by Strand (@strandnerd).</description>
    <link>https://dev.to/strandnerd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3593736%2Fdcb15278-65aa-438e-aa4b-687249e59150.png</url>
      <title>DEV Community: Strand</title>
      <link>https://dev.to/strandnerd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/strandnerd"/>
    <language>en</language>
    <item>
      <title>Building a Firecracker VM Orchestrator in Go - Part 2: API Server</title>
      <dc:creator>Strand</dc:creator>
      <pubDate>Tue, 31 Mar 2026 02:11:55 +0000</pubDate>
      <link>https://dev.to/strandnerd/building-a-firecracker-vm-orchestrator-in-go-part-2-api-server-28ip</link>
      <guid>https://dev.to/strandnerd/building-a-firecracker-vm-orchestrator-in-go-part-2-api-server-28ip</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you missed the first post in this series, &lt;a&gt;start here&lt;/a&gt;. It covers the foundation: five provider interfaces that decouple the Flames control plane from any specific infrastructure backend.&lt;/p&gt;

&lt;p&gt;This time we're building the first thing that actually &lt;em&gt;does something&lt;/em&gt;: the &lt;strong&gt;API server&lt;/strong&gt;. This is the first real consumer of those provider interfaces. A running binary. &lt;code&gt;go run ./cmd/flames-api&lt;/code&gt;, curl against it, create a VM. That kind of thing.&lt;/p&gt;

&lt;p&gt;But what made this spec interesting wasn't the HTTP handlers, it was the design conversation I had with Claude Code along the way. I pushed back on several decisions, changed the architecture in meaningful ways, and the final result is quite different from what was initially proposed. That's the part worth documenting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Steering
&lt;/h2&gt;

&lt;p&gt;Here's the thing about working with AI agents on architecture: the first draft is usually reasonable but generic. The value I bring is knowing where generic breaks down. Let me walk through the key moments where I steered the design.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Make transport an interface"
&lt;/h3&gt;

&lt;p&gt;The initial spec described an HTTP API server. Handlers, routes, JSON. Standard stuff. But I've been down this road. You build everything into HTTP handlers, then six months later someone asks for gRPC, and you're refactoring the entire service layer.&lt;/p&gt;

&lt;p&gt;So I pushed back: &lt;strong&gt;the transport needs to be a swappable layer&lt;/strong&gt;. The business logic should live in a &lt;code&gt;Service&lt;/code&gt; struct that speaks only domain types. HTTP is just the first adapter. gRPC, WebSocket, whatever, they're all just different ways to call the same methods.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpnt4rdube2w34gcn7kk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpnt4rdube2w34gcn7kk.png" alt="Transport Layers" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This changed the entire structure. Instead of one package with handlers that embed business logic, we got three clean layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;api/&lt;/code&gt;&lt;/strong&gt;: Pure business logic. No &lt;code&gt;net/http&lt;/code&gt;, no JSON tags, no transport concepts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;transport/httpapi/&lt;/code&gt;&lt;/strong&gt;: Thin HTTP adapter. Decodes requests, calls service, encodes responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cmd/flames-api/&lt;/code&gt;&lt;/strong&gt;: Wires it all together.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  "Why use the colon style?"
&lt;/h3&gt;

&lt;p&gt;The initial design proposed &lt;code&gt;POST /v1/vms/{vm_id}:stop&lt;/code&gt;, Google's API design convention with colons for custom actions. I asked a simple question: if it's unusual and requires careful &lt;code&gt;ServeMux&lt;/code&gt; registration, why not just use &lt;code&gt;/stop&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;There was no good reason. We changed it to &lt;code&gt;POST /v1/vms/{vm_id}/stop&lt;/code&gt;. Sometimes the right architectural decision is just removing complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Why not add limit to ListControllers now?"
&lt;/h3&gt;

&lt;p&gt;The design had a risk section noting "No pagination on ListControllers, acceptable for now, will need it later." I pointed out that &lt;code&gt;ListEvents&lt;/code&gt; already had filtering via &lt;code&gt;EventFilter&lt;/code&gt;, so why defer the same pattern for controllers?&lt;/p&gt;

&lt;p&gt;That led to adding &lt;code&gt;ControllerFilter&lt;/code&gt; (with &lt;code&gt;Status&lt;/code&gt; and &lt;code&gt;Limit&lt;/code&gt; fields), which meant extending the &lt;code&gt;StateStore&lt;/code&gt; interface from SPEC-001 with a new &lt;code&gt;ListControllers&lt;/code&gt; method. A small change now that avoids a breaking change later.&lt;/p&gt;

&lt;h3&gt;
  
  
  "In-memory for tests, SQLite for dev, Postgres for prod"
&lt;/h3&gt;

&lt;p&gt;The idempotency store risk mentioned "when persistence is added." I corrected the framing. The project isn't going from "in-memory" to "persistent." It's a three-tier model: in-memory for tests, SQLite for local dev, Postgres (or other databases) for prod. Every stateful component should follow this progression.&lt;/p&gt;

&lt;p&gt;This is the kind of context that lives in my head but needs to be explicit in the spec so the AI (and future contributors) make the right calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spec
&lt;/h2&gt;

&lt;p&gt;Like SPEC-001, I shared the main ideas and core points (what the API should do, which endpoints, what the transport story should look like) and Claude Opus 4.6 turned that into three structured spec notes. I steer, it writes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/flames-hq/flames/blob/main/.contextpin/notes/Specs/API%20Server/Requirements.md" rel="noopener noreferrer"&gt;Requirements&lt;/a&gt;&lt;/strong&gt;: 8 user stories, 23 functional requirements split across service layer, HTTP transport, and infrastructure. Plus non-functional requirements and 13 acceptance criteria.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/flames-hq/flames/blob/main/.contextpin/notes/Specs/API%20Server/Design.md" rel="noopener noreferrer"&gt;Design&lt;/a&gt;&lt;/strong&gt;: Package layout, dependency graph, service method signatures, HTTP route mapping, error mapping strategy, idempotency design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/flames-hq/flames/blob/main/.contextpin/notes/Specs/API%20Server/Tasks.md" rel="noopener noreferrer"&gt;Tasks&lt;/a&gt;&lt;/strong&gt;: 10 tasks across 4 phases with dependency tracking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A few highlights from the spec:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;User Stories&lt;/strong&gt;: The transport-independence story was the one I added after steering the design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;US-1: As an operator, I want to create a VM via an API...&lt;/li&gt;
&lt;li&gt;US-7: As a developer, I want to start a fully functional API server with &lt;code&gt;go run ./cmd/flames-api&lt;/code&gt;...&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;US-8: As a platform team, I want to swap the transport layer without rewriting business logic...&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Functional Requirements&lt;/strong&gt;: Split into three sections reflecting the layered architecture:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service Layer&lt;/td&gt;
&lt;td&gt;FR-001 to FR-012&lt;/td&gt;
&lt;td&gt;Business logic, event emission, limit enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP Transport&lt;/td&gt;
&lt;td&gt;FR-013 to FR-020&lt;/td&gt;
&lt;td&gt;Routes, error mapping, idempotency, healthz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;FR-021 to FR-023&lt;/td&gt;
&lt;td&gt;Entry point, flags, testability&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Non-Functional&lt;/strong&gt;: The stdlib-only constraint got nuanced: the service layer and HTTP transport must be stdlib-only, but future transports (gRPC) may bring their own dependencies scoped to their package.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0js92xfthnxslt1lzjg9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0js92xfthnxslt1lzjg9.png" alt="Architecture" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The service is a concrete struct, not an interface. This was a deliberate call in the design. There's only one business logic, you don't need polymorphism for "the thing that does the work." Transports call its methods directly. If we ever need a service interface (middleware chaining, for example), we extract it then. Not before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API
&lt;/h2&gt;

&lt;p&gt;All mutations return &lt;strong&gt;202 Accepted&lt;/strong&gt; because the desired state is recorded, but convergence happens asynchronously. This is fundamental to how Flames works: you tell the system what you want, and the reconciler makes it happen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fayt4pk7hxnahjszbwepd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fayt4pk7hxnahjszbwepd.png" alt="API Sequence Diagram" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Route Map
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Service Method&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/vms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CreateVM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;202&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/vms/{vm_id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GetVM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/vms/{vm_id}/stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;StopVM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;202&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/vms/{vm_id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeleteVM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;202&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/controllers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RegisterController&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/controllers/{id}/heartbeat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Heartbeat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/controllers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ListControllers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/events&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ListEvents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/healthz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Idempotency
&lt;/h3&gt;

&lt;p&gt;Mutation endpoints support an &lt;code&gt;Idempotency-Key&lt;/code&gt; header. Same key + same body replays the original response. Same key + different body returns 409. The implementation uses SHA-256 body hashing with a &lt;code&gt;ResponseWriter&lt;/code&gt; wrapper to capture responses before they're sent. It's a transport concern, lives entirely in &lt;code&gt;transport/httpapi/&lt;/code&gt;, not in the service layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Responses
&lt;/h3&gt;

&lt;p&gt;Errors from the provider layer map cleanly to HTTP: &lt;code&gt;ErrNotFound&lt;/code&gt; becomes 404, &lt;code&gt;ErrAlreadyExists&lt;/code&gt; and &lt;code&gt;ErrConflict&lt;/code&gt; become 409, anything else is 500.&lt;/p&gt;

&lt;p&gt;Every error response is structured JSON with &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;resource_type&lt;/code&gt;, and &lt;code&gt;resource_id&lt;/code&gt;. No string matching on the client side. This is the same &lt;code&gt;providererr&lt;/code&gt; pattern from SPEC-001 surfaced through HTTP. The structured errors we designed in the foundation layer pay off immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;p&gt;10 tasks, 4 phases, all executed in a single session. &lt;strong&gt;7 files created&lt;/strong&gt;, 4 existing files modified. Zero external dependencies. Every test passes with &lt;code&gt;-race&lt;/code&gt;. The binary starts, you curl it, VMs get created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflections on the Workflow
&lt;/h2&gt;

&lt;p&gt;This spec was more interesting than SPEC-001 because the steering mattered more. Provider interfaces are fairly standard Go, the AI can nail those with minimal guidance. But an API server involves architectural judgment calls: where does business logic live? What's a transport concern vs. a service concern? Do you build for flexibility now or later?&lt;/p&gt;

&lt;p&gt;The answer was different for different things. Transport abstraction? Build it now, the cost is near zero and the payoff is real. Colon-style routes? Don't bother. Pagination on controllers? Add it now, same pattern already exists.&lt;/p&gt;

&lt;p&gt;These are 30-second decisions in a conversation, but they compound into a fundamentally different codebase. The AI proposes, I steer, the spec captures the decision, and the implementation follows. That's the loop.&lt;/p&gt;

&lt;p&gt;What I like about the Spec-Driven workflow on ContextPin is that these decisions are &lt;em&gt;recorded&lt;/em&gt;. The Requirements note shows &lt;em&gt;what&lt;/em&gt; we decided. The Design note shows &lt;em&gt;why&lt;/em&gt;. If someone reads the spec six months from now, they'll understand not just the code but the reasoning behind it. That's the difference between documentation and a spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The API server is running but nobody's talking to it yet. The next specs will bring the system to life: controllers that actually boot Firecracker VMs, a scheduler that assigns VMs to controllers, and persistence so state survives a restart. The provider interfaces and service layer are ready for all of it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of the Flames Spec-Driven Development series.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;This entire project is being built using &lt;strong&gt;ContextPin + Claude Code&lt;/strong&gt;. 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, &lt;a href="https://contextpin.com" rel="noopener noreferrer"&gt;you can join here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>ai</category>
      <category>virtualmachine</category>
    </item>
    <item>
      <title>Building a Firecracker VM Orchestrator in Go - Part 1: Provider Interfaces</title>
      <dc:creator>Strand</dc:creator>
      <pubDate>Mon, 30 Mar 2026 00:42:38 +0000</pubDate>
      <link>https://dev.to/strandnerd/building-a-firecracker-vm-orchestrator-in-go-part-1-provider-interfaces-11io</link>
      <guid>https://dev.to/strandnerd/building-a-firecracker-vm-orchestrator-in-go-part-1-provider-interfaces-11io</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I'm building &lt;strong&gt;Flames&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;p&gt;I've been coding with AI agents for a while now, but what's different this time is that I'm using &lt;strong&gt;&lt;a href="https://contextpin.com" rel="noopener noreferrer"&gt;ContextPin&lt;/a&gt;&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frb2cdllhaj1yzg3cc01z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frb2cdllhaj1yzg3cc01z.png" alt="Spec-Driven Development Workflow" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm documenting the whole journey here.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This first spec tackles the most foundational piece: the &lt;strong&gt;provider interfaces&lt;/strong&gt;. Five abstractions that decouple the entire control plane from any specific infrastructure backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;go run&lt;/code&gt; locally shouldn't need a database cluster. A production deployment should plug in real backends without changing application code.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fukkozdqt2d4f4zh590ee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fukkozdqt2d4f4zh590ee.png" alt=" " width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spec
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of Go, I wrote three structured notes in ContextPin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/flames-hq/flames/blob/main/.contextpin/notes/Specs/Provider%20Interfaces/Requirements.md" rel="noopener noreferrer"&gt;Requirements&lt;/a&gt;&lt;/strong&gt; — 7 user stories, 30+ functional requirements, acceptance criteria, and non-functional constraints. This is the contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/flames-hq/flames/blob/main/.contextpin/notes/Specs/Provider%20Interfaces/Design.md" rel="noopener noreferrer"&gt;Design&lt;/a&gt;&lt;/strong&gt; — Package layout, dependency graph, data models, interface signatures, implementation notes, and risk analysis. This is the blueprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/flames-hq/flames/blob/main/.contextpin/notes/Specs/Provider%20Interfaces/Tasks.md" rel="noopener noreferrer"&gt;Tasks&lt;/a&gt;&lt;/strong&gt; — The ordered implementation checklist derived from the design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Interfaces
&lt;/h2&gt;

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

&lt;p&gt;Each interface lives in its own package, imports only from &lt;code&gt;model/&lt;/code&gt; and &lt;code&gt;provider/providererr/&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Design Decisions
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interface-per-package over monolithic provider
&lt;/h3&gt;

&lt;p&gt;One interface per package rather than a single &lt;code&gt;Provider&lt;/code&gt; 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 — &lt;code&gt;provider/state/postgres&lt;/code&gt; imports &lt;code&gt;provider/state&lt;/code&gt; and nothing else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Non-blocking &lt;code&gt;Dequeue&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;WorkQueue.Dequeue&lt;/code&gt; returns &lt;code&gt;ErrNoJobs&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conformance test suites
&lt;/h3&gt;

&lt;p&gt;This is the one I'm most excited about. Every interface has a shared test suite in &lt;code&gt;providertest/&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured errors with &lt;code&gt;errors.Is&lt;/code&gt; support
&lt;/h3&gt;

&lt;p&gt;Provider errors carry metadata (resource type, ID) and support &lt;code&gt;errors.Is&lt;/code&gt; matching against sentinels like &lt;code&gt;ErrNotFound&lt;/code&gt;, &lt;code&gt;ErrConflict&lt;/code&gt;, &lt;code&gt;ErrCacheMiss&lt;/code&gt;. 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflections on the Workflow
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of the Flames Spec-Driven Development series.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;This entire project is being built using &lt;strong&gt;ContextPin + Claude Code&lt;/strong&gt;. 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, &lt;a href="https://contextpin.com" rel="noopener noreferrer"&gt;you can join here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>What is Spec-Driven Development?</title>
      <dc:creator>Strand</dc:creator>
      <pubDate>Wed, 25 Mar 2026 17:38:15 +0000</pubDate>
      <link>https://dev.to/strandnerd/what-is-spec-driven-development-4oo4</link>
      <guid>https://dev.to/strandnerd/what-is-spec-driven-development-4oo4</guid>
      <description>&lt;p&gt;If you ask a coding agent to build something meaningful, there is a good chance it will do a surprisingly good job. And at this point, I do not even mean only small tasks. These tools can already build large chunks of a product, entire features, and sometimes even a full website or app with very solid results.&lt;/p&gt;

&lt;p&gt;That part is real.&lt;/p&gt;

&lt;p&gt;I use coding agents a lot, and I think this is exactly why so many people get excited right away. You give the agent a direction, it starts moving fast, and the output can be genuinely impressive.&lt;/p&gt;

&lt;p&gt;But the real challenge is not whether AI can handle scope. It clearly can.&lt;/p&gt;

&lt;p&gt;The challenge is how that scope is defined, structured, and kept aligned as the project grows.&lt;/p&gt;

&lt;p&gt;A larger project is not just "more code." It is more decisions, more constraints, more moving parts, more tradeoffs, and more chances for small misunderstandings to compound into bigger problems.&lt;/p&gt;

&lt;p&gt;When I build products, I am not thinking only about the next function or the next component. I am thinking about the shape of the system, the tradeoffs, the constraints, the future changes, the developer experience, the boundaries between parts, and what I want the final product to become.&lt;/p&gt;

&lt;p&gt;To me, that is the difference.&lt;/p&gt;

&lt;p&gt;As a software engineer, I do not want to keep nudging the wheel left and right every five seconds while the car is already moving. I want to design the road.&lt;/p&gt;

&lt;p&gt;That is why spec-driven development feels so important right now.&lt;/p&gt;

&lt;p&gt;It gives me a way to turn intent into something explicit before implementation becomes the source of truth by accident. Instead of jumping straight into code and hoping the agent infers the rest, I can define what should exist, how it should behave, what constraints matter, what is out of scope, and what the architecture should protect.&lt;/p&gt;

&lt;p&gt;That changes everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spec-driven development is not new
&lt;/h2&gt;

&lt;p&gt;One thing I think is important to say is that spec-driven development is not some brand new AI-era invention.&lt;/p&gt;

&lt;p&gt;The idea of making behavior, contracts, and intent explicit before or alongside implementation has been around for decades. What is new is that AI makes the payoff much more obvious. A spec is no longer just documentation for humans. It is also high-value input for agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  A quick timeline
&lt;/h3&gt;

&lt;p&gt;Spec-driven development is not new at all. The idea has shown up in different forms for decades.&lt;/p&gt;

&lt;p&gt;In 1969, formal reasoning about programs introduced the idea that software could be described in terms of clear expected behavior, not just written and tested afterward.&lt;/p&gt;

&lt;p&gt;Later, formal specification methods and Design by Contract pushed this idea further by making system behavior and constraints explicit before or alongside implementation.&lt;/p&gt;

&lt;p&gt;Then practices like Test-Driven Development and Behavior-Driven Development made specs more practical for everyday teams by turning expected behavior into tests and human-readable scenarios.&lt;/p&gt;

&lt;p&gt;After that, API-first development with Swagger and OpenAPI brought spec-first thinking into mainstream backend development by making contracts machine-readable and implementation-friendly.&lt;/p&gt;

&lt;p&gt;Now with AI coding agents, the same idea matters even more. A spec is no longer just helpful for humans. It also gives agents a much clearer structure to follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters so much now with AI
&lt;/h2&gt;

&lt;p&gt;This is the part that feels different in practice.&lt;/p&gt;

&lt;p&gt;Before AI, a spec was already valuable. It aligned people, clarified behavior, and reduced mistakes.&lt;/p&gt;

&lt;p&gt;But now a spec also acts like a control surface for an agent.&lt;/p&gt;

&lt;p&gt;That means I can do something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the problem clearly.&lt;/li&gt;
&lt;li&gt;Design the system shape.&lt;/li&gt;
&lt;li&gt;Break the work into tasks.&lt;/li&gt;
&lt;li&gt;Iterate with the agent.&lt;/li&gt;
&lt;li&gt;Review the result against the spec.&lt;/li&gt;
&lt;li&gt;Refine the spec or the implementation and repeat.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That loop is incredibly powerful.&lt;/p&gt;

&lt;p&gt;Without a spec, I am often asking the model to guess what I mean from a half-formed prompt and a pile of code. Sometimes it still does well. But once the task gets bigger, or touches architecture, data flow, boundaries, naming, migration strategy, edge cases, or product intent, I have seen the quality drop fast.&lt;/p&gt;

&lt;p&gt;The agent starts filling gaps with assumptions.&lt;/p&gt;

&lt;p&gt;And that is the real problem.&lt;/p&gt;

&lt;p&gt;AI is great at execution inside a constrained frame. But if I want the output to match my product vision, I need to provide the frame.&lt;/p&gt;

&lt;p&gt;That is how I think about spec-driven development today. It is not bureaucracy. It is not writing giant documents for the sake of it. It is creating the minimum structured thinking needed so humans and agents can build in the same direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I mean by "spec"
&lt;/h2&gt;

&lt;p&gt;When I say spec, I do not just mean one giant requirements doc.&lt;/p&gt;

&lt;p&gt;A spec can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a feature brief&lt;/li&gt;
&lt;li&gt;architecture notes&lt;/li&gt;
&lt;li&gt;contracts and interfaces&lt;/li&gt;
&lt;li&gt;acceptance criteria&lt;/li&gt;
&lt;li&gt;non-goals&lt;/li&gt;
&lt;li&gt;task breakdowns&lt;/li&gt;
&lt;li&gt;examples of expected behavior&lt;/li&gt;
&lt;li&gt;migration constraints&lt;/li&gt;
&lt;li&gt;edge cases&lt;/li&gt;
&lt;li&gt;naming decisions&lt;/li&gt;
&lt;li&gt;API schemas&lt;/li&gt;
&lt;li&gt;invariants&lt;/li&gt;
&lt;li&gt;notes about why a tradeoff was made&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, a spec is the explicit shape of intent.&lt;/p&gt;

&lt;p&gt;That shape can be lightweight or deep depending on the problem.&lt;/p&gt;

&lt;p&gt;For a tiny one-off task, I may not need much.&lt;/p&gt;

&lt;p&gt;For a real product, I absolutely do.&lt;/p&gt;

&lt;h2&gt;
  
  
  My experience with this
&lt;/h2&gt;

&lt;p&gt;The more I use coding agents, the more I feel this personally.&lt;/p&gt;

&lt;p&gt;When I go straight from idea to code, things can move quickly, but I also get more drift. Naming drifts. Architecture drifts. The agent solves the local problem but misses the bigger system constraints. I end up spending more time correcting direction later.&lt;/p&gt;

&lt;p&gt;When I spend time on planning first, the entire interaction changes.&lt;/p&gt;

&lt;p&gt;The agent becomes more useful because I am no longer asking it to invent the project structure, the product intent, and the engineering standards from thin air. I am giving it a map.&lt;/p&gt;

&lt;p&gt;And honestly, I think this is where a lot of the real value is.&lt;/p&gt;

&lt;p&gt;Not just "AI writes code."&lt;/p&gt;

&lt;p&gt;More like: "I define the system clearly enough that AI can execute without constantly losing the thread."&lt;/p&gt;

&lt;p&gt;That is a much better workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is also why I'm building ContextPin
&lt;/h2&gt;

&lt;p&gt;A big reason I care so much about this is because I think the tooling still feels incomplete.&lt;/p&gt;

&lt;p&gt;A lot of AI coding tools are optimized for fast prompting and code generation, but not enough for long-lived context, specs, notes, design decisions, and local project knowledge that should stay close to the repo.&lt;/p&gt;

&lt;p&gt;I want something better for that workflow.&lt;/p&gt;

&lt;p&gt;So I'm building an ADE, an Agentic Development Environment, around this idea.&lt;/p&gt;

&lt;p&gt;It is GPU accelerated and native. No Electron, no Tauri, no browser wrapper.&lt;/p&gt;

&lt;p&gt;The focus is spec-driven development.&lt;/p&gt;

&lt;p&gt;The idea is that I can keep notes, context, and structured project thinking inside the repo, versioned with Git, local-first, and ready for both humans and agents. Not hidden in random chat history. Not scattered across docs that never stay in sync. Not locked away from the actual development loop.&lt;/p&gt;

&lt;p&gt;I want the spec, the design notes, the task breakdown, and the implementation context to live closer together.&lt;/p&gt;

&lt;p&gt;That is the workflow I want for myself, so that is what I'm building.&lt;/p&gt;

&lt;p&gt;Early access here: &lt;a href="https://contextpin.com" rel="noopener noreferrer"&gt;contextpin.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;AI can help a lot with implementation, but I still believe the most important work is deciding what should be built, how it should be shaped, what constraints matter, and how the pieces should evolve over time.&lt;/p&gt;

&lt;p&gt;That is the work of designing the road.&lt;/p&gt;

&lt;p&gt;And in a world full of increasingly capable coding agents, I think specs are becoming one of the best ways to make that intent concrete.&lt;/p&gt;

&lt;p&gt;Not because the idea is new.&lt;/p&gt;

&lt;p&gt;But because now the payoff is impossible to ignore.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>agents</category>
      <category>agentskills</category>
    </item>
  </channel>
</rss>
