Appearance
Spec-Driven Development with AI
Introduction
Spec-Driven Development (SDD) with AI is a modern software engineering methodology where formal specifications — such as OpenAPI documents, JSON Schema definitions, protocol buffers, or structured requirement documents — serve as the single source of truth that drives AI-assisted code generation, validation, and testing. By coupling rigorous specifications with large language models and code-generation pipelines, teams can dramatically accelerate delivery while maintaining correctness guarantees. This approach transforms specifications from passive documentation into active, executable contracts that govern every phase of the software lifecycle.
Core Concepts
What Is Spec-Driven Development?
Traditional development often treats specifications as afterthoughts — documents written after the code exists, perpetually out of date. Spec-Driven Development inverts this: the spec comes first, and everything else is derived from it.
The core loop is:
- Author a formal specification (OpenAPI, AsyncAPI, JSON Schema, Protobuf, or structured natural language)
- Validate the spec for correctness and completeness
- Generate code artifacts (server stubs, client SDKs, DTOs, tests) from the spec
- Implement business logic within the generated scaffolding
- Verify that the implementation conforms to the spec via contract testing
Where AI Enters the Picture
AI amplifies every stage of this loop:
| Stage | Traditional SDD | AI-Enhanced SDD |
|---|---|---|
| Authoring | Manual YAML/JSON writing | LLM generates spec from natural language requirements |
| Validation | Schema linters | AI detects semantic inconsistencies, missing edge cases |
| Code Generation | Template-based (Swagger Codegen) | AI produces idiomatic, context-aware implementations |
| Testing | Manual test writing | AI generates comprehensive test suites from spec |
| Documentation | Static rendering | AI generates tutorials, examples, migration guides |
The Specification as Contract
A specification acts as a contract between:
- Frontend and Backend teams
- Service A and Service B in a microservices architecture
- Your API and external consumers
- Human developers and AI code generators
This contract is machine-readable, version-controlled, and enforceable.
Types of Specifications in SDD
Building an AI-Powered Spec-Driven Pipeline
Phase 1: Spec Authoring with AI
The first step is translating human intent into a formal specification. Below is a Java application that uses an LLM to generate an OpenAPI spec from a natural language description.
java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;
public class SpecGenerator {
private static final String LLM_ENDPOINT = System.getenv("LLM_API_ENDPOINT");
private static final String API_KEY = System.getenv("LLM_API_KEY");
private static final ObjectMapper JSON = new ObjectMapper();
private static final YAMLMapper YAML = new YAMLMapper();
/**
* Generates an OpenAPI 3.0 specification from natural language requirements.
*/
public String generateSpec(String naturalLanguageRequirement) throws Exception {
String systemPrompt = """
You are an API architect. Given a natural language description of an API,
produce a complete OpenAPI 3.0.3 specification in YAML format.
Include:
- All CRUD endpoints
- Request/response schemas with examples
- Error responses (400, 401, 404, 500)
- Pagination for list endpoints
- Authentication scheme (Bearer JWT)
Respond with ONLY valid YAML, no explanation.
""";
Map<String, Object> payload = Map.of(
"model", "claude-sonnet-4-20250514",
"max_tokens", 4096,
"messages", new Object[]{
Map.of("role", "system", "content", systemPrompt),
Map.of("role", "user", "content", naturalLanguageRequirement)
}
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(LLM_ENDPOINT))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(payload)))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("LLM API error: " + response.statusCode()
+ " - " + response.body());
}
// Extract the YAML content from the LLM response
JsonNode body = JSON.readTree(response.body());
String specYaml = body.at("/content/0/text").asText();
// Validate it's parseable YAML
YAML.readTree(specYaml);
return specYaml;
}
public static void main(String[] args) throws Exception {
SpecGenerator generator = new SpecGenerator();
String requirement = """
Build a Task Management API for a project management tool.
Users can create projects, add tasks to projects, assign tasks
to team members, and track task status (TODO, IN_PROGRESS, DONE).
Tasks have a title, description, priority (LOW, MEDIUM, HIGH),
due date, and assignee. Support filtering tasks by status and priority.
""";
String spec = generator.generateSpec(requirement);
System.out.println(spec);
}
}Phase 2: Spec Validation
Once a spec is generated, it must be validated both syntactically and semantically.
java
import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import java.util.ArrayList;
import java.util.List;
public class SpecValidator {
public record ValidationResult(
boolean valid,
List<String> syntaxErrors,
List<String> semanticWarnings
) {}
/**
* Validates an OpenAPI spec for syntax and common semantic issues.
*/
public ValidationResult validate(String openApiYaml) {
List<String> syntaxErrors = new ArrayList<>();
List<String> semanticWarnings = new ArrayList<>();
// Step 1: Parse and validate syntax
SwaggerParseResult result = new OpenAPIParser()
.readContents(openApiYaml, null, null);
if (result.getMessages() != null) {
syntaxErrors.addAll(result.getMessages());
}
OpenAPI api = result.getOpenAPI();
if (api == null) {
syntaxErrors.add("Failed to parse OpenAPI document");
return new ValidationResult(false, syntaxErrors, semanticWarnings);
}
// Step 2: Semantic checks
if (api.getPaths() != null) {
api.getPaths().forEach((path, pathItem) -> {
// Check: Every non-GET endpoint should have a request body
if (pathItem.getPost() != null
&& pathItem.getPost().getRequestBody() == null) {
semanticWarnings.add(
"POST " + path + " has no request body defined");
}
// Check: Every endpoint should have error responses
if (pathItem.getGet() != null
&& pathItem.getGet().getResponses() != null) {
var responses = pathItem.getGet().getResponses();
if (!responses.containsKey("404")
&& path.contains("{")) {
semanticWarnings.add(
"GET " + path + " with path params missing 404 response");
}
}
// Check: List endpoints should support pagination
if (pathItem.getGet() != null
&& !path.contains("{")
&& pathItem.getGet().getParameters() != null) {
boolean hasPagination = pathItem.getGet().getParameters()
.stream()
.anyMatch(p -> "limit".equals(p.getName())
|| "offset".equals(p.getName())
|| "page".equals(p.getName()));
if (!hasPagination) {
semanticWarnings.add(
"GET " + path + " (collection) missing pagination params");
}
}
});
}
// Check: Security scheme defined
if (api.getComponents() == null
|| api.getComponents().getSecuritySchemes() == null
|| api.getComponents().getSecuritySchemes().isEmpty()) {
semanticWarnings.add("No security schemes defined");
}
boolean valid = syntaxErrors.isEmpty();
return new ValidationResult(valid, syntaxErrors, semanticWarnings);
}
public static void main(String[] args) {
String sampleSpec = """
openapi: 3.0.3
info:
title: Task API
version: 1.0.0
paths:
/tasks:
get:
summary: List tasks
responses:
'200':
description: OK
/tasks/{id}:
get:
summary: Get task
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
""";
SpecValidator validator = new SpecValidator();
ValidationResult result = validator.validate(sampleSpec);
System.out.println("Valid: " + result.valid());
System.out.println("Syntax Errors: " + result.syntaxErrors());
System.out.println("Semantic Warnings: " + result.semanticWarnings());
}
}Phase 3: AI-Driven Code Generation from Spec
This is where AI surpasses traditional template-based generators. Instead of producing boilerplate, an LLM can generate idiomatic, well-structured code that includes error handling, logging, and design patterns.
java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Orchestrates AI-driven code generation from an OpenAPI specification.
* Parses the spec, breaks it into generation tasks, and invokes an LLM
* to produce idiomatic Java code for each component.
*/
public class AICodeGenerator {
public record GenerationTask(String type, String name, String context) {}
public record GeneratedFile(String path, String content) {}
private final SpecParser specParser;
private final LLMClient llmClient;
public AICodeGenerator(SpecParser specParser, LLMClient llmClient) {
this.specParser = specParser;
this.llmClient = llmClient;
}
/**
* Generates a complete Spring Boot project from an OpenAPI spec.
*/
public List<GeneratedFile> generate(String openApiYaml) throws Exception {
List<GeneratedFile> files = new ArrayList<>();
var spec = specParser.parse(openApiYaml);
// Generate DTOs from schemas
for (var schema : spec.schemas()) {
String prompt = buildDtoPrompt(schema);
String code = llmClient.complete(prompt);
files.add(new GeneratedFile(
"src/main/java/com/example/dto/" + schema.name() + "Dto.java",
code
));
}
// Generate Controllers, Services, and Repositories per resource
for (var resource : spec.resources()) {
// Controller
String controllerPrompt = buildControllerPrompt(resource, spec);
files.add(new GeneratedFile(
"src/main/java/com/example/controller/"
+ resource.name() + "Controller.java",
llmClient.complete(controllerPrompt)
));
// Service interface + implementation
String servicePrompt = buildServicePrompt(resource);
files.add(new GeneratedFile(
"src/main/java/com/example/service/"
+ resource.name() + "Service.java",
llmClient.complete(servicePrompt)
));
// Tests
String testPrompt = buildTestPrompt(resource, spec);
files.add(new GeneratedFile(
"src/test/java/com/example/controller/"
+ resource.name() + "ControllerTest.java",
llmClient.complete(testPrompt)
));
}
return files;
}
private String buildDtoPrompt(SchemaDefinition schema) {
return """
Generate a Java 17 record-based DTO for this schema:
Name: %s
Fields: %s
Include:
- Jakarta Validation annotations
- Jackson annotations for serialization
- A builder pattern
- Javadoc
Respond with ONLY Java code.
""".formatted(schema.name(), schema.fields());
}
private String buildControllerPrompt(
ResourceDefinition resource, ParsedSpec spec) {
return """
Generate a Spring Boot REST controller for resource: %s
Endpoints: %s
Related schemas: %s
Include:
- Proper HTTP status codes
- Input validation with @Valid
- Exception handling with @ControllerAdvice pattern
- Pagination for list endpoints
- OpenAPI annotations (@Operation, @ApiResponse)
- Slf4j logging
Respond with ONLY Java code.
""".formatted(
resource.name(),
resource.endpoints(),
spec.schemasForResource(resource.name())
);
}
// Additional prompt builders omitted for brevity...
private String buildServicePrompt(ResourceDefinition resource) {
return "Generate service for " + resource.name();
}
private String buildTestPrompt(
ResourceDefinition resource, ParsedSpec spec) {
return "Generate tests for " + resource.name();
}
/**
* Writes all generated files to disk.
*/
public void writeToDisk(
List<GeneratedFile> files, Path outputDir) throws IOException {
for (var file : files) {
Path target = outputDir.resolve(file.path());
Files.createDirectories(target.getParent());
Files.writeString(target, file.content());
System.out.println("Generated: " + target);
}
}
// Placeholder types for compilation
record SchemaDefinition(String name, List<Map<String,String>> fields) {}
record ResourceDefinition(String name, List<String> endpoints) {}
record ParsedSpec(
List<SchemaDefinition> schemas,
List<ResourceDefinition> resources,
List<String> schemasForResource(String name)
) {}
interface SpecParser { ParsedSpec parse(String yaml); }
interface LLMClient { String complete(String prompt) throws Exception; }
}The Feedback Loop: Contract Testing
The generated code must continuously be verified against the spec. This is the enforcement mechanism that makes SDD reliable.
java
import io.restassured.RestAssured;
import io.restassured.response.Response;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Contract tester that validates a running API against its OpenAPI spec.
* Ensures every endpoint returns the correct status codes, headers,
* and response schema shapes.
*/
public class ContractTester {
public record ContractViolation(String endpoint, String message) {}
private final String baseUrl;
private final JsonNode spec;
public ContractTester(String baseUrl, Path specPath) throws Exception {
this.baseUrl = baseUrl;
String yaml = Files.readString(specPath);
this.spec = new YAMLMapper().readTree(yaml);
}
/**
* Runs contract validation for all GET endpoints.
*/
public List<ContractViolation> validateGetEndpoints() {
List<ContractViolation> violations = new ArrayList<>();
JsonNode paths = spec.get("paths");
paths.fieldNames().forEachRemaining(path -> {
JsonNode pathItem = paths.get(path);
if (pathItem.has("get")) {
JsonNode getOp = pathItem.get("get");
String resolvedPath = resolvePathWithExamples(path);
String url = baseUrl + resolvedPath;
try {
Response response = RestAssured.get(url);
// Check status code is in spec
String statusCode = String.valueOf(response.statusCode());
if (!getOp.get("responses").has(statusCode)) {
violations.add(new ContractViolation(
"GET " + path,
"Unexpected status code: " + statusCode
));
}
// Check content type
String contentType = response.contentType();
if (contentType != null
&& !contentType.contains("application/json")) {
violations.add(new ContractViolation(
"GET " + path,
"Expected application/json, got: " + contentType
));
}
// Check response body matches schema shape
if (response.statusCode() == 200) {
validateResponseShape(
violations, "GET " + path,
response.body().asString(), getOp);
}
} catch (Exception e) {
violations.add(new ContractViolation(
"GET " + path,
"Request failed: " + e.getMessage()
));
}
}
});
return violations;
}
private String resolvePathWithExamples(String path) {
// Replace {id} with a test value
return path.replaceAll("\\{[^}]+\\}", "test-id-123");
}
private void validateResponseShape(
List<ContractViolation> violations,
String endpoint, String body, JsonNode operation) {
// Simplified shape check — a full implementation would
// resolve $ref and validate against JSON Schema
try {
new com.fasterxml.jackson.databind.ObjectMapper().readTree(body);
} catch (Exception e) {
violations.add(new ContractViolation(
endpoint, "Response is not valid JSON"));
}
}
public static void main(String[] args) throws Exception {
ContractTester tester = new ContractTester(
"http://localhost:8080",
Path.of("api-spec.yaml")
);
List<ContractViolation> violations = tester.validateGetEndpoints();
if (violations.isEmpty()) {
System.out.println("✅ All contracts satisfied!");
} else {
System.out.println("❌ Contract violations found:");
violations.forEach(v ->
System.out.printf(" [%s] %s%n", v.endpoint(), v.message()));
System.exit(1);
}
}
}Architecture of an SDD+AI Platform
Spec Evolution and Versioning
One of the hardest challenges in SDD is evolving specifications without breaking consumers. AI can analyze spec diffs and predict breaking changes.
AI-Generated Test Suites from Spec
java
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Generates comprehensive test cases from an OpenAPI specification.
* Uses AI to create edge cases that a template-based generator would miss.
*/
public class AITestGenerator {
public record TestCase(
String name,
String httpMethod,
String path,
Map<String, String> headers,
String requestBody,
int expectedStatus,
String assertionDescription
) {}
/**
* Generates test cases for a single endpoint by analyzing
* the spec's constraints, types, and examples.
*/
public List<TestCase> generateTestCases(
String method, String path,
Map<String, Object> endpointSpec) {
List<TestCase> tests = new ArrayList<>();
// Happy path
tests.add(new TestCase(
method + "_" + sanitize(path) + "_success",
method, path,
Map.of("Authorization", "Bearer valid-token",
"Content-Type", "application/json"),
generateValidBody(endpointSpec),
200,
"Should return 200 with valid request"
));
// Missing auth
tests.add(new TestCase(
method + "_" + sanitize(path) + "_unauthorized",
method, path,
Map.of("Content-Type", "application/json"),
generateValidBody(endpointSpec),
401,
"Should return 401 when Authorization header is missing"
));
// Invalid body (for POST/PUT)
if ("POST".equals(method) || "PUT".equals(method)) {
tests.add(new TestCase(
method + "_" + sanitize(path) + "_bad_request",
method, path,
Map.of("Authorization", "Bearer valid-token",
"Content-Type", "application/json"),
"{\"invalid\": true}",
400,
"Should return 400 with invalid request body"
));
// Boundary values for numeric fields
tests.add(new TestCase(
method + "_" + sanitize(path) + "_boundary_values",
method, path,
Map.of("Authorization", "Bearer valid-token",
"Content-Type", "application/json"),
generateBoundaryBody(endpointSpec),
400,
"Should validate boundary values"
));
}
// Not found (for paths with parameters)
if (path.contains("{")) {
String notFoundPath = path.replaceAll(
"\\{[^}]+\\}", "nonexistent-id-999");
tests.add(new TestCase(
method + "_" + sanitize(path) + "_not_found",
method, notFoundPath,
Map.of("Authorization", "Bearer valid-token"),
null,
404,
"Should return 404 for nonexistent resource"
));
}
return tests;
}
private String sanitize(String path) {
return path.replaceAll("[^a-zA-Z0-9]", "_")
.replaceAll("_+", "_");
}
private String generateValidBody(Map<String, Object> spec) {
return "{\"title\": \"Test Task\", " +
"\"description\": \"A valid task\", " +
"\"priority\": \"MEDIUM\"}";
}
private String generateBoundaryBody(Map<String, Object> spec) {
return "{\"title\": \"\", \"priority\": \"INVALID\"}";
}
public static void main(String[] args) {
AITestGenerator generator = new AITestGenerator();
List<TestCase> tests = generator.generateTestCases(
"POST", "/api/tasks",
Map.of("requestBody", Map.of("required", true))
);
tests.forEach(tc ->
System.out.printf("[%s] %s %s → expect %d: %s%n",
tc.name(), tc.httpMethod(), tc.path(),
tc.expectedStatus(), tc.assertionDescription()));
}
}Maturity Model for SDD with AI
Common Anti-Patterns
Best Practices
Spec First, Always: Never write implementation code before the spec is reviewed and approved. The spec is the design document, the contract, and the test oracle.
Version Your Specs Semantically: Use semver for specs — MAJOR for breaking changes, MINOR for additive features, PATCH for documentation fixes. Automate version bumps based on diff analysis.
Treat AI Output as Draft: Every AI-generated artifact — spec, code, or test — must pass human review. AI accelerates creation but cannot substitute for domain expertise and judgment.
Enforce Contracts in CI/CD: Contract tests should be mandatory in your pipeline. A build that passes unit tests but violates the spec should fail.
Use RAG for Organization Context: Feed your organization's coding standards, architecture patterns, and existing APIs into a retrieval-augmented generation (RAG) pipeline so AI output matches your conventions.
Modularize Large Specs: Break monolithic specifications into domain-bounded documents that reference each other via
$ref. This improves AI generation quality since smaller specs produce more focused code.Maintain a Spec Registry: Store all specs in a centralized, searchable registry. This prevents duplication and helps AI understand the full API landscape when generating new services.
Automate Backward Compatibility Checks: Use tools like
oasdiffor AI-powered analyzers to detect breaking changes before they merge. Block breaking changes unless accompanied by a migration plan.Generate Documentation from Spec, Not Code: Let the spec drive your API docs (Swagger UI, Redoc) rather than generating docs from code annotations. This ensures docs always match the contract.
Iterate with Tight Feedback Loops: After AI generates code, immediately run contract tests. Feed failures back to the AI as context for the next generation attempt. This creates a self-improving cycle.
Related Concepts
- REST HTTP Verbs and Status Codes: Fundamental HTTP semantics that OpenAPI specs describe
- OAuth: Authentication schemes commonly specified in OpenAPI security definitions
- Asynchronous Programming: Patterns for async API implementations generated from AsyncAPI specs
- Serverless and Container Workloads: Deployment targets for spec-generated services
- Open/Closed Principle: AI-generated code should be open for extension — spec-driven interfaces enable this naturally
- Dependency Inversion Principle: Generated code should depend on abstractions (interfaces from specs), not concrete implementations