stub-service/docs/adr/adr-001.md

21 KiB

ADR-001: Architecture of stub-service — Go-based HTTP Stub/Mock Service

Status: Proposed
Date: 2026-05-07
Authors: backend-agent


Context

Our development and QA workflows require a service-virtualization tool that can stand in for real HTTP services during integration testing, local development, and demo environments. Mountebank is the de-facto open-source option, but it presents several pain points for our team:

  1. Node.js runtime — adds a runtime dependency our Go/Java-centric infrastructure doesn't otherwise need.
  2. No built-in Web UI — managing imposters requires curl/Postman or third-party UIs.
  3. No AI-assisted authoring — creating complex stubs is manual and error-prone.
  4. Limited extensibility — behaviours and predicates are plugin-based in JavaScript, which is awkward to extend from our stack.

We need a lightweight, single-binary alternative written in Go that keeps Mountebank's core concepts (imposters, stubs, predicates, responses, proxies) while adding a web UI and LLM-powered stub generation.


Decision

Build stub-service — a Go application that provides:

  • An HTTP stub server with rich request matching
  • A REST management API (CRUD for imposters/stubs)
  • A web UI for visual management and real-time request logging
  • An LLM adapter for natural-language stub authoring
  • Proxy mode with record/replay capability

High-Level Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                        stub-service process                         │
│                                                                     │
│  ┌──────────────┐   ┌──────────────┐   ┌────────────────────────┐  │
│  │  Management   │   │   Web UI     │   │   Stub Listeners       │  │
│  │  REST API     │   │  (embedded)  │   │   (dynamic ports)      │  │
│  │  :8080/api/v1 │   │  :8080/ui    │   │   :N per imposter      │  │
│  └──────┬───────┘   └──────┬───────┘   └───────────┬────────────┘  │
│         │                  │                        │               │
│         ▼                  ▼                        ▼               │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                     Core Engine                              │   │
│  │  ┌─────────────┐  ┌──────────┐  ┌───────────┐  ┌────────┐  │   │
│  │  │  Imposter   │  │ Matcher  │  │ Response  │  │ Proxy  │  │   │
│  │  │  Manager    │  │ Engine   │  │ Renderer  │  │ Module │  │   │
│  │  └─────────────┘  └──────────┘  └───────────┘  └────────┘  │   │
│  └──────────────────────────┬──────────────────────────────────┘   │
│                             │                                       │
│         ┌───────────────────┼────────────────────┐                 │
│         ▼                   ▼                    ▼                  │
│  ┌─────────────┐   ┌──────────────┐   ┌─────────────────┐         │
│  │  Stub Store │   │ Request Log  │   │  LLM Adapter    │         │
│  │  (SQLite /  │   │   (ring buf) │   │  (Anthropic /   │         │
│  │   bbolt)    │   │              │   │   OpenAI)       │         │
│  └─────────────┘   └──────────────┘   └─────────────────┘         │
└─────────────────────────────────────────────────────────────────────┘

Component Breakdown

1. Management REST API (internal/api)

The control plane. All imposter/stub CRUD operations happen here.

  • Listens on a single admin port (default :8080), path prefix /api/v1
  • JSON request/response bodies
  • Endpoints (see API Design section below)
  • Provides SSE endpoint for real-time request log streaming

2. Web UI (internal/ui)

An embedded single-page application served from the same admin port under /ui.

  • Technology: Templ templates + htmx + minimal CSS (e.g., Pico CSS or similar classless framework)
  • Rationale: No JavaScript build step, no npm dependency. Templ compiles to Go, htmx provides dynamic behavior via HTML attributes. The result is a server-rendered UI that feels interactive without a JS framework.
  • Pages:
    • Dashboard — list of all imposters with status, port, hit counts
    • Imposter detail — list of stubs, predicates, responses, recent requests
    • Stub editor — form-based creation/editing of stubs with predicate/response builders
    • Request log — real-time log of incoming requests (SSE-powered)
    • LLM assistant — text input for natural-language stub creation

3. Stub Listeners (internal/server)

Each imposter binds to a dedicated port and handles incoming traffic.

  • Dynamically started/stopped when imposters are created/deleted
  • Each listener runs an HTTP handler that delegates to the Matcher Engine
  • TLS support via optional per-imposter cert/key configuration

4. Core Engine (internal/engine)

The domain logic of the application.

  • Imposter Manager — lifecycle management of imposters (create, update, delete, start, stop). Coordinates listener creation and stub store persistence.
  • Matcher Engine — evaluates incoming requests against stub predicates. Returns the first matching stub or a configurable default response. Supports predicate composition (and, or, not).
  • Response Renderer — produces the HTTP response from a stub definition. Handles static bodies, Go template rendering (with request data as context), latency injection, and header manipulation.
  • Proxy Module — forwards requests to a real upstream, optionally records the response as a new stub for future replay.

5. Stub Store (internal/store)

Persistence layer for imposters, stubs, and request logs.

  • Primary: SQLite via modernc.org/sqlite (pure Go, no CGO) or bbolt for a simpler key-value approach.
  • Decision: SQLite preferred — it supports rich queries for filtering stubs and request logs, and the schema maps naturally to the relational data model.
  • Migrations managed via embedded SQL files (embed package).
  • Backup: the SQLite file can be copied while the service runs (WAL mode).
  • Optional file-based export/import in JSON format for portability.

6. LLM Adapter (internal/llm)

Translates natural-language descriptions into stub configurations.

  • Interface: A StubGenerator interface that takes a text prompt and returns a structured stub definition.
  • Implementation: HTTP client calling the Anthropic Messages API (Claude) or OpenAI Chat Completions API, configurable via environment variables.
  • Prompt strategy: System prompt contains the stub JSON schema and examples. User message is the natural-language description. The LLM returns a JSON stub definition that is validated before being applied.
  • Flow:
    1. User types: "Return 200 with a JSON user profile when GET /api/users/123 is called"
    2. LLM adapter sends the prompt with schema context
    3. Response is parsed and validated against the stub schema
    4. If valid, the stub is presented to the user for confirmation in the Web UI
    5. On confirmation, the stub is saved via the Management API

7. Request Log (internal/log)

In-memory ring buffer of recent requests per imposter, with optional persistence to SQLite.

  • Stores: timestamp, method, path, headers, body (truncated), matched stub ID, response status, latency
  • Queryable via Management API (filter by imposter, time range, match status)
  • Streamed to the Web UI via SSE

Technology Choices

Concern Choice Rationale
Language Go 1.23+ Single binary, excellent HTTP stdlib, team expertise
HTTP framework net/http (stdlib) + chi router Minimal dependency, chi adds route params and middleware without abstracting stdlib
Web UI Templ + htmx + Pico CSS No JS build toolchain, server-rendered, interactive via htmx
Database SQLite (modernc.org/sqlite) Pure Go, zero CGO, rich SQL queries, single file
LLM client Direct HTTP (Anthropic SDK or net/http) Avoid heavy SDK deps; a thin wrapper suffices
Config Environment variables + optional YAML file 12-factor, simple for Docker
Logging log/slog (stdlib) Structured logging, zero deps
Testing testing + testify Assertions, standard tooling
TLS crypto/tls (stdlib) Per-imposter certs for HTTPS stubs

API Design Overview

Base URL: http://localhost:8080/api/v1

Imposters

Method Path Description
GET /imposters List all imposters
POST /imposters Create a new imposter
GET /imposters/{id} Get imposter details
PUT /imposters/{id} Update imposter
DELETE /imposters/{id} Delete imposter and stop listener
DELETE /imposters Delete all imposters
POST /imposters/{id}/start Start the stub listener
POST /imposters/{id}/stop Stop the stub listener

Stubs (nested under imposter)

Method Path Description
GET /imposters/{id}/stubs List stubs for an imposter
POST /imposters/{id}/stubs Add a stub
PUT /imposters/{id}/stubs/{stubId} Update a stub
DELETE /imposters/{id}/stubs/{stubId} Delete a stub

Request Log

Method Path Description
GET /imposters/{id}/requests Get request log (paginated)
GET /imposters/{id}/requests/stream SSE stream of live requests
DELETE /imposters/{id}/requests Clear request log

LLM

Method Path Description
POST /generate Generate stub config from natural-language prompt

System

Method Path Description
GET /health Health check
GET /config Current configuration (redacted)
POST /import Import imposters from JSON file
GET /export Export all imposters as JSON

Data Model

Imposter

{
  "id": "uuid",
  "name": "User Service Mock",
  "protocol": "http",
  "port": 4545,
  "tls": {
    "cert": "...",
    "key": "..."
  },
  "default_response": {
    "status": 404,
    "headers": {"Content-Type": "application/json"},
    "body": "{\"error\": \"no matching stub\"}"
  },
  "stubs": [ ... ],
  "state": "running",
  "created_at": "2026-05-07T10:00:00Z",
  "request_count": 142
}

Stub

{
  "id": "uuid",
  "imposter_id": "uuid",
  "priority": 1,
  "predicates": [ ... ],
  "response": { ... },
  "hit_count": 37,
  "created_at": "2026-05-07T10:00:00Z"
}

Predicate

Predicates are composable. Each predicate has a type and type-specific fields:

{
  "type": "equals",
  "field": "path",
  "value": "/api/users/123"
}
{
  "type": "regex",
  "field": "path",
  "value": "^/api/users/\\d+$"
}
{
  "type": "jsonpath",
  "expression": "$.order.items[?(@.quantity > 5)]",
  "exists": true
}
{
  "type": "and",
  "predicates": [
    {"type": "equals", "field": "method", "value": "POST"},
    {"type": "contains", "field": "body", "value": "\"action\":\"create\""}
  ]
}

Supported predicate types:

  • equals — exact match on method, path, header, query param, or body
  • contains — substring match
  • regex — regular expression match
  • jsonpath — JSONPath expression evaluation
  • xpath — XPath expression evaluation (for XML bodies)
  • and — all child predicates must match
  • or — at least one child predicate must match
  • not — invert a child predicate

Fields available for matching: method, path, query.<param>, header.<name>, body

Response

{
  "status": 200,
  "headers": {
    "Content-Type": "application/json"
  },
  "body": "{\"id\": 123, \"name\": \"Alice\"}",
  "template": false,
  "latency_ms": 0,
  "proxy": null
}

When template: true, the body is rendered as a Go template with the request context:

{
  "echo": "You requested {{.Request.Path}} with method {{.Request.Method}}"
}

Proxy Response

{
  "proxy": {
    "to": "https://real-service.example.com",
    "mode": "record",
    "add_wait_behavior": true
  }
}

Proxy modes:

  • passthrough — forward and return, do not record
  • record — forward, return, and save as a new stub for future replay
  • replay — use previously recorded response if available, otherwise forward

SQLite Schema (initial)

CREATE TABLE imposters (
    id          TEXT PRIMARY KEY,
    name        TEXT NOT NULL,
    protocol    TEXT NOT NULL DEFAULT 'http',
    port        INTEGER NOT NULL UNIQUE,
    tls_cert    TEXT,
    tls_key     TEXT,
    default_response_json TEXT,
    state       TEXT NOT NULL DEFAULT 'stopped',
    created_at  TEXT NOT NULL,
    updated_at  TEXT NOT NULL
);

CREATE TABLE stubs (
    id          TEXT PRIMARY KEY,
    imposter_id TEXT NOT NULL REFERENCES imposters(id) ON DELETE CASCADE,
    priority    INTEGER NOT NULL DEFAULT 0,
    predicates_json TEXT NOT NULL,
    response_json   TEXT NOT NULL,
    hit_count   INTEGER NOT NULL DEFAULT 0,
    created_at  TEXT NOT NULL,
    updated_at  TEXT NOT NULL
);

CREATE INDEX idx_stubs_imposter ON stubs(imposter_id, priority);

CREATE TABLE request_log (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    imposter_id TEXT NOT NULL REFERENCES imposters(id) ON DELETE CASCADE,
    timestamp   TEXT NOT NULL,
    method      TEXT NOT NULL,
    path        TEXT NOT NULL,
    headers_json TEXT,
    body        TEXT,
    matched_stub_id TEXT,
    response_status INTEGER,
    latency_ms  INTEGER,
    created_at  TEXT NOT NULL
);

CREATE INDEX idx_request_log_imposter_ts ON request_log(imposter_id, timestamp DESC);

Project Layout

stub-service/
├── cmd/
│   └── stub-service/
│       └── main.go              # Entry point, config loading, wiring
├── internal/
│   ├── api/                     # Management REST API handlers
│   │   ├── handler.go
│   │   ├── imposter.go
│   │   ├── stub.go
│   │   ├── requestlog.go
│   │   ├── generate.go          # LLM generation endpoint
│   │   └── middleware.go
│   ├── engine/                  # Core domain logic
│   │   ├── imposter.go          # Imposter manager
│   │   ├── matcher.go           # Predicate matching engine
│   │   ├── renderer.go          # Response rendering (static, template)
│   │   └── proxy.go             # Proxy/record/replay
│   ├── model/                   # Domain types
│   │   ├── imposter.go
│   │   ├── stub.go
│   │   ├── predicate.go
│   │   └── response.go
│   ├── store/                   # Persistence
│   │   ├── store.go             # Store interface
│   │   ├── sqlite.go            # SQLite implementation
│   │   └── migrations/          # Embedded SQL migrations
│   │       └── 001_init.sql
│   ├── server/                  # Dynamic stub HTTP listeners
│   │   └── listener.go
│   ├── llm/                     # LLM integration
│   │   ├── generator.go         # StubGenerator interface
│   │   ├── anthropic.go         # Anthropic/Claude implementation
│   │   └── openai.go            # OpenAI implementation
│   ├── ui/                      # Web UI
│   │   ├── templates/           # Templ files
│   │   ├── static/              # CSS, minimal JS (htmx)
│   │   └── handler.go           # UI route handlers
│   └── config/                  # Configuration loading
│       └── config.go
├── docs/
│   └── adr/
│       └── adr-001.md           # This document
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yaml
├── Makefile
└── README.md

Deployment Model

Docker (primary):

# Build stage
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /stub-service ./cmd/stub-service

# Runtime stage
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=build /stub-service /usr/local/bin/stub-service
EXPOSE 8080
VOLUME /data
ENTRYPOINT ["stub-service"]

docker-compose.yaml (local dev):

services:
  stub-service:
    build: .
    ports:
      - "8080:8080"        # Admin API + Web UI
      - "4545-4600:4545-4600"  # Range for stub listeners
    volumes:
      - stub-data:/data
    environment:
      STUB_ADMIN_PORT: "8080"
      STUB_DATA_DIR: "/data"
      STUB_LOG_LEVEL: "debug"
      LLM_PROVIDER: "anthropic"
      LLM_API_KEY: "${LLM_API_KEY}"
      LLM_MODEL: "claude-sonnet-4-6"

volumes:
  stub-data:

Configuration (environment variables):

Variable Default Description
STUB_ADMIN_PORT 8080 Admin API and Web UI port
STUB_DATA_DIR ./data SQLite database directory
STUB_LOG_LEVEL info Log level (debug, info, warn, error)
STUB_PORT_RANGE_START 4545 Start of port range for stub listeners
STUB_PORT_RANGE_END 4600 End of port range for stub listeners
LLM_PROVIDER (none) LLM provider: anthropic or openai
LLM_API_KEY (none) LLM API key
LLM_MODEL claude-sonnet-4-6 Model to use for stub generation

Consequences

Positive

  • Single binary — no runtime dependencies beyond the OS; trivial to deploy.
  • Embedded Web UI — no separate frontend build/deploy pipeline; Templ + htmx keeps it all in Go.
  • SQLite — zero operational overhead, no external database to manage, easy backup (copy a file).
  • LLM integration — significantly lowers the barrier for creating complex stubs, especially for QA engineers unfamiliar with JSONPath/regex predicates.
  • Mountebank-compatible concepts — teams familiar with Mountebank can transfer their mental model (imposters, stubs, predicates).
  • Port-per-imposter isolation — each mock service gets its own port, matching how real services are addressed.

Negative

  • Port range management — dynamic port allocation requires firewall/Docker port-range configuration.
  • SQLite single-writer — under very heavy concurrent management API load, SQLite's single-writer lock could be a bottleneck. Acceptable for a testing/dev tool.
  • LLM dependency — the generation feature requires external API access and incurs cost. It is optional — the service works fully without it.
  • No multi-protocol support initially — only HTTP/HTTPS. TCP/SMTP stubs (which Mountebank supports) are deferred to a future iteration.

Risks

  • Templ maturity — Templ is relatively new. Mitigation: the UI is simple enough that switching to html/template is feasible with moderate effort.
  • JSONPath/XPath complexity — Go's ecosystem for JSONPath and XPath is less mature than Node.js. Mitigation: use well-maintained libraries (github.com/PaesslerAG/jsonpath, github.com/antchfx/xmlquery) and test thoroughly.

References