<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pixel Piston</title><description>blog</description><link>https://pixel-pistons.vercel.app/</link><language>en</language><item><title>Spec Driven Development: A Practical Guide with SpecKit</title><link>https://pixel-pistons.vercel.app/posts/spec-kit/spec-driven-development-guide/</link><guid isPermaLink="true">https://pixel-pistons.vercel.app/posts/spec-kit/spec-driven-development-guide/</guid><description>Learn how spec-driven development can transform your workflow from chaos to clarity. A deep dive into SpecKit with real examples and lessons learned.</description><pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Spec-Driven Development: A Practical Guide with SpecKit&lt;/h1&gt;
&lt;p&gt;Starting features with enthusiasm and ending three days later wondering what you were actually building is common. Spec-driven development fixes this.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;A feature request arrives: &quot;add comments to blog posts.&quot; Your brain jumps to implementation: GitHub API, markdown rendering, localStorage caching.&lt;/p&gt;
&lt;p&gt;Two weeks later:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Caching is overly complex&lt;/li&gt;
&lt;li&gt;Repo name hardcoded in five places&lt;/li&gt;
&lt;li&gt;No rate limit handling&lt;/li&gt;
&lt;li&gt;Mobile layout broken&lt;/li&gt;
&lt;li&gt;Forgot the &quot;disable comments per post&quot; requirement&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The issue isn&apos;t coding skills—it&apos;s jumping into implementation before understanding what you&apos;re building.&lt;/p&gt;
&lt;h2&gt;Spec-Driven Development&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Write a detailed specification before writing code.&lt;/strong&gt; This isn&apos;t waterfall documentation. It&apos;s lightweight and practical.&lt;/p&gt;
&lt;p&gt;The process:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Specify&lt;/strong&gt; what you&apos;re building (what and why, not how)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan&lt;/strong&gt; how you&apos;ll build it (technical decisions, trade-offs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Break down&lt;/strong&gt; into actionable tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implement&lt;/strong&gt; with confidence&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each step forces thinking at the right abstraction level. You clarify user needs before designing schemas. You understand constraints before picking libraries.&lt;/p&gt;
&lt;h2&gt;SpecKit&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/speckit/speckit&quot;&gt;SpecKit&lt;/a&gt; codifies this workflow with opinionated structure.&lt;/p&gt;
&lt;p&gt;Four main commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;speckit specify   # Create the feature specification
speckit plan      # Generate the implementation plan
speckit tasks     # Break down into actionable tasks
speckit implement # Execute the tasks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each command builds on the previous one. Here&apos;s a real example.&lt;/p&gt;
&lt;h2&gt;Example: Adding Giscus Comments&lt;/h2&gt;
&lt;p&gt;I recently added comments to this blog. Instead of diving into code, I used SpecKit.&lt;/p&gt;
&lt;h3&gt;Step 1: Specification&lt;/h3&gt;
&lt;p&gt;Ran &lt;code&gt;speckit specify&lt;/code&gt; with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Comment section under each post using GitHub comments&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;SpecKit generated:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Feature Specification: Giscus Comments Integration

## Feature Overview

Enable blog post comments using **Giscus** - a comments system 
powered by GitHub Discussions. Readers can discuss posts using 
their GitHub accounts without leaving the blog.

## User Stories

### US1: Display Giscus Comments Widget (P1)

**As a** blog reader  
**I want to** see a comments section under blog posts  
**So that** I can read and participate in discussions

**Acceptance Criteria**:
- Giscus widget loads on posts where commentsEnabled: true
- Widget displays existing discussion comments from GitHub
- Widget matches blog&apos;s light/dark theme automatically
- Loading state shows graceful placeholder
- Widget is responsive on all screen sizes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice what&apos;s missing: no React vs Vue, no database schemas, no API endpoints. Just user-facing requirements.&lt;/p&gt;
&lt;p&gt;The spec included non-goals:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Non-Goals

- Custom comment UI (use Giscus default)
- Email notifications (handled by GitHub)  
- Comment editing within blog (use GitHub Discussions)
- Anonymous comments (GitHub authentication required)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Non-goals define scope boundaries. Write them down so future-you doesn&apos;t waste time on features you explicitly decided against.&lt;/p&gt;
&lt;h3&gt;Step 2: Implementation Plan&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;speckit plan&lt;/code&gt; generated the technical plan—actual engineering decisions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Implementation Plan: Giscus Comments Integration

## Constitution Compliance Check

✅ Preservation-First: Fuwari template structure untouched
✅ Minimal Change: Single new component
✅ Component Isolation: Zero dependencies on existing components
✅ Configuration Over Code: All settings in src/config.ts
✅ Development Validation: TypeScript strict mode enabled

## Tech Stack Analysis

**Existing Stack**:
- Framework: Astro 5.x (SSG)
- UI: Svelte 5
- Styling: Tailwind CSS 3.x
- Package Manager: pnpm

**New Dependencies**:
- Giscus: CDN-hosted script (no npm dependencies needed)

## Implementation Strategy

### Phase 1: Setup &amp;amp; Configuration
- Create src/types/giscus.ts
- Add giscusConfig to src/config.ts
- Extend content schema

### Phase 2: Component Implementation
- Create GiscusComments.svelte
- Dynamic script injection via onMount
- Handle theme synchronization
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The plan also showed file changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/
├── components/
│   └── GiscusComments.svelte          (NEW - 50 lines)
├── types/
│   └── giscus.ts                       (NEW - 20 lines)
├── config.ts                           (MODIFY - add 15 lines)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before writing code, I knew I&apos;d touch three files and add about 85 lines. No surprises.&lt;/p&gt;
&lt;h3&gt;Step 3: Task Breakdown&lt;/h3&gt;
&lt;p&gt;Running &lt;code&gt;speckit tasks&lt;/code&gt; broke the work into 47 concrete tasks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Phase 1: Setup &amp;amp; Type Definitions

- [ ] T001 Create specs/002-giscus-comments/ directory structure
- [ ] T002 Create src/types/giscus.ts with interfaces
- [ ] T003 Verify existing dependencies

## Phase 2: Configuration &amp;amp; Schema

- [ ] T004 Add giscusConfig export to src/config.ts
- [ ] T005 Extend postsCollection schema with commentsEnabled field
- [ ] T006 Update .gitignore if needed

## Phase 3: Component Implementation

- [ ] T007 [US1] Create GiscusComments.svelte skeleton
- [ ] T008 [US1] Implement onMount hook for script injection
- [ ] T009 [US1] Set all data-* attributes from config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each task is actionable, testable, and scoped. Tasks are tagged with user stories &lt;code&gt;[US1]&lt;/code&gt; for tracking.&lt;/p&gt;
&lt;h3&gt;Step 4: Implementation&lt;/h3&gt;
&lt;p&gt;I implemented manually using the task list as a guide. One evening instead of the usual weekend of refactoring.&lt;/p&gt;
&lt;p&gt;Final component (50 lines):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script lang=&quot;ts&quot;&amp;gt;
  import { onMount } from &quot;svelte&quot;;
  import { giscusConfig } from &quot;@/config&quot;;

  interface Props {
    discussionNumber?: number;
    class?: string;
  }

  let { discussionNumber, class: className = &quot;&quot; }: Props = $props();
  let container: HTMLDivElement;

  onMount(() =&amp;gt; {
    const script = document.createElement(&quot;script&quot;);
    script.src = &quot;https://giscus.app/client.js&quot;;
    script.setAttribute(&quot;data-repo&quot;, giscusConfig.repo);
    script.setAttribute(&quot;data-repo-id&quot;, giscusConfig.repoId);
    script.setAttribute(&quot;data-category&quot;, giscusConfig.category);
    script.setAttribute(&quot;data-category-id&quot;, giscusConfig.categoryId);

    if (discussionNumber) {
      script.setAttribute(&quot;data-mapping&quot;, &quot;number&quot;);
      script.setAttribute(&quot;data-discussion-number&quot;, discussionNumber.toString());
    } else {
      script.setAttribute(&quot;data-mapping&quot;, giscusConfig.mapping);
    }

    script.setAttribute(&quot;data-strict&quot;, &quot;0&quot;);
    script.setAttribute(&quot;data-reactions-enabled&quot;, 
      giscusConfig.reactionsEnabled ? &quot;1&quot; : &quot;0&quot;);
    script.setAttribute(&quot;data-theme&quot;, giscusConfig.theme);
    script.setAttribute(&quot;data-lang&quot;, giscusConfig.lang);
    script.crossOrigin = &quot;anonymous&quot;;
    script.async = true;

    container.appendChild(script);
  });
&amp;lt;/script&amp;gt;

&amp;lt;div bind:this={container} class=&quot;giscus-container w-full {className}&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clean, simple, no scope creep. Just the feature, shipped.&lt;/p&gt;
&lt;h2&gt;vs. Other Methodologies&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Test-Driven Development (TDD)&lt;/strong&gt;: Write tests first, implement to pass. Great for algorithms and correctness, but doesn&apos;t help figure out &lt;em&gt;what&lt;/em&gt; to build. TDD starts at code level; SDD starts at product level. They&apos;re complementary.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Behavior-Driven Development (BDD)&lt;/strong&gt;: Uses Gherkin scenarios to bridge product and engineering. Similar to SDD but test-focused. Specs are broader—they include architecture, trade-offs, and non-functional requirements.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Documentation-First&lt;/strong&gt;: Usually either too detailed (becomes outdated immediately) or too vague (doesn&apos;t constrain implementation). Specs are the middle ground.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ad-Hoc Development&lt;/strong&gt;: Works for prototypes, very small features, or problems you&apos;ve solved repeatedly. For everything else, you end up refactoring because requirements weren&apos;t thought through.&lt;/p&gt;
&lt;h2&gt;Benefits and Trade-offs&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clarity before code&lt;/li&gt;
&lt;li&gt;Easy PR reviews with context&lt;/li&gt;
&lt;li&gt;Onboarding documentation&lt;/li&gt;
&lt;li&gt;Less refactoring (catch design flaws in planning)&lt;/li&gt;
&lt;li&gt;Better estimates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upfront time investment&lt;/li&gt;
&lt;li&gt;Overhead for tiny changes&lt;/li&gt;
&lt;li&gt;Can feel slow at first&lt;/li&gt;
&lt;li&gt;Requires discipline&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key is knowing when to use it. Greenfield features? Yes. Bug fixes? No. Infrastructure changes? Yes. Typo in README? No.&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# Install SpecKit
npm install -g speckit-cli

# Initialize in your project
cd your-project
speckit init

# Create your first spec
speckit specify &quot;Add user authentication with OAuth2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SpecKit interviews you about the feature, generates a spec. Edit it, then:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;speckit plan    # Generate implementation plan
speckit tasks   # Break into tasks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Implement manually or use &lt;code&gt;speckit implement&lt;/code&gt; for AI assistance.&lt;/p&gt;
&lt;h2&gt;Lessons Learned&lt;/h2&gt;
&lt;p&gt;After six months:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Specs evolve&lt;/strong&gt;: Your first spec won&apos;t be perfect. Update it as you discover edge cases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Write for future-you&lt;/strong&gt;: Document decisions. You won&apos;t remember why in three months.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep specs focused&lt;/strong&gt;: If it&apos;s 10+ pages, it&apos;s either multiple features or too detailed. Good specs are 1-3 pages, readable in 10 minutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use a constitution&lt;/strong&gt;: Document development principles. SpecKit can check plans against it. Mine includes: preserve template structure, minimal changes, configuration over code, component isolation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Customize templates&lt;/strong&gt;: Codify your team&apos;s practices. My templates include constitution compliance, performance considerations, security implications, and rollback plans.&lt;/p&gt;
&lt;h2&gt;When NOT to Use This&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Prototypes (you&apos;re exploring and learning)&lt;/li&gt;
&lt;li&gt;Trivial changes (typo fixes)&lt;/li&gt;
&lt;li&gt;Emergency fixes (production down? Fix first, spec later)&lt;/li&gt;
&lt;li&gt;Solo side projects (overhead may not be worth it)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The sweet spot: &lt;strong&gt;greenfield features&lt;/strong&gt; in &lt;strong&gt;production systems&lt;/strong&gt; with &lt;strong&gt;multiple contributors&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;Spec-driven development forces you to think before coding. SpecKit codifies a workflow good teams already follow informally.&lt;/p&gt;
&lt;p&gt;If you&apos;re getting lost mid-implementation or constantly clarifying requirements, try it. Next feature, spend 30 minutes writing a spec—just user stories and acceptance criteria. See how it feels.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;: &lt;a href=&quot;https://speckit.dev&quot;&gt;SpecKit Docs&lt;/a&gt; • &lt;a href=&quot;https://specdriven.dev&quot;&gt;Spec-Driven Development Manifesto&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Building AI-Powered Spring Boot Apps with Spring AI</title><link>https://pixel-pistons.vercel.app/posts/spring-ai/spring-boot-spring-ai-guide/</link><guid isPermaLink="true">https://pixel-pistons.vercel.app/posts/spring-ai/spring-boot-spring-ai-guide/</guid><description>A practical guide to integrating AI into Spring Boot applications using Spring AI, with real code examples and production patterns.</description><pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building AI-Powered Spring Boot Apps with Spring AI&lt;/h1&gt;
&lt;p&gt;If you&apos;re working with Spring Boot and feeling left out of the AI wave, don&apos;t. Spring AI brings AI integration to the Spring ecosystem without forcing you to rewrite everything in Python.&lt;/p&gt;
&lt;p&gt;I&apos;ll walk through building an AI powered REST API using real code. We&apos;ll create an article summarizer that actually works in production.&lt;/p&gt;
&lt;h2&gt;Why Bother?&lt;/h2&gt;
&lt;p&gt;Your stack is already Spring Boot. Your team knows Spring. Your infrastructure runs Spring. Why add Python microservices or learn new frameworks when Spring AI works with what you have?&lt;/p&gt;
&lt;p&gt;You get familiar patterns (&lt;code&gt;ChatClient&lt;/code&gt; feels like &lt;code&gt;RestTemplate&lt;/code&gt;), dependency injection, auto configuration, and support for multiple AI providers like OpenAI, Azure, Anthropic, or local models via Ollama.&lt;/p&gt;
&lt;h2&gt;The Project&lt;/h2&gt;
&lt;p&gt;We&apos;re building a REST API that takes articles and returns AI-generated summaries. You&apos;ll be able to specify summary length and get actual production-ready code.&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;Here are the Maven dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;project&amp;gt;
    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;3.2.0&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;
    
    &amp;lt;dependencies&amp;gt;
        &amp;lt;!-- Spring Boot Web for REST API --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        
        &amp;lt;!-- Spring AI for OpenAI integration --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.ai&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-ai-openai-spring-boot-starter&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.0.0-M1&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        
        &amp;lt;!-- For API documentation (optional but recommended) --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springdoc&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;springdoc-openapi-starter-webmvc-ui&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;2.3.0&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Configuration in &lt;code&gt;application.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4
          temperature: 0.7
          max-tokens: 500

# Application-specific settings
app:
  ai:
    rate-limit:
      requests-per-minute: 10
    timeout-seconds: 30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set your API key as an environment variable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export OPENAI_API_KEY=sk-your-key-here
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Core Concepts&lt;/h2&gt;
&lt;h3&gt;ChatClient&lt;/h3&gt;
&lt;p&gt;The main interface. Send a string, get a string back:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Autowired
private ChatClient chatClient;

public String chat(String message) {
    return chatClient.call(message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ChatOptions&lt;/h3&gt;
&lt;p&gt;Control AI behavior with options:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ChatOptions options = OpenAiChatOptions.builder()
    .withModel(&quot;gpt-4&quot;)
    .withTemperature(0.7)  // 0 = deterministic, 1 = creative
    .withMaxTokens(500)
    .build();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Temperature&lt;/strong&gt; controls randomness (0 = deterministic, 1 = creative). &lt;strong&gt;Max tokens&lt;/strong&gt; limits response length. Set these globally or per-request.&lt;/p&gt;
&lt;h3&gt;PromptTemplate&lt;/h3&gt;
&lt;p&gt;Don&apos;t hardcode prompts. Use templates:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String template = &quot;&quot;&quot;
    Summarize the following article in {length} sentences.
    
    Title: {title}
    Content: {content}
    
    Summary:
    &quot;&quot;&quot;;

PromptTemplate promptTemplate = new PromptTemplate(template);
Prompt prompt = promptTemplate.create(Map.of(
    &quot;length&quot;, &quot;3&quot;,
    &quot;title&quot;, article.getTitle(),
    &quot;content&quot;, article.getContent()
));

String summary = chatClient.call(prompt).getResult();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Building the Summarizer&lt;/h2&gt;
&lt;h3&gt;Domain Model&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.example.aisummary.model;

public record Article(String title, String content) {
    public Article {
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException(&quot;Title cannot be blank&quot;);
        }
        if (content == null || content.isBlank()) {
            throw new IllegalArgumentException(&quot;Content cannot be blank&quot;);
        }
    }
}

public record SummaryRequest(Article article, SummaryLength length) {}

public enum SummaryLength {
    SHORT(3),
    MEDIUM(5),
    LONG(10);
    
    private final int sentences;
    
    SummaryLength(int sentences) {
        this.sentences = sentences;
    }
    
    public int getSentences() {
        return sentences;
    }
}

public record SummaryResponse(String summary, int tokensUsed) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Service&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.example.aisummary.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class ArticleSummarizerService {

    private static final String SUMMARY_TEMPLATE = &quot;&quot;&quot;
        You are a professional editor summarizing articles for busy readers.
        
        Summarize the following article in exactly {length} concise sentences.
        Focus on the main points and key takeaways.
        
        Article Title: {title}
        
        Article Content:
        {content}
        
        Summary (exactly {length} sentences):
        &quot;&quot;&quot;;

    private final ChatClient chatClient;
    private final PromptTemplate promptTemplate;

    public ArticleSummarizerService(ChatClient chatClient) {
        this.chatClient = chatClient;
        this.promptTemplate = new PromptTemplate(SUMMARY_TEMPLATE);
    }

    public SummaryResponse summarize(SummaryRequest request) {
        // Build the prompt with variables
        Prompt prompt = promptTemplate.create(Map.of(
            &quot;title&quot;, request.article().title(),
            &quot;content&quot;, truncateContent(request.article().content()),
            &quot;length&quot;, String.valueOf(request.length().getSentences())
        ));

        // Configure for this specific request
        OpenAiChatOptions options = OpenAiChatOptions.builder()
            .withTemperature(0.5)  // Lower for consistent summaries
            .withMaxTokens(calculateMaxTokens(request.length()))
            .build();

        // Make the AI call
        ChatResponse response = chatClient.call(
            new Prompt(prompt.getContents(), options)
        );

        // Extract the result
        String summary = response.getResult()
            .getOutput()
            .getContent();
        
        int tokensUsed = response.getMetadata()
            .getUsage()
            .getTotalTokens();

        return new SummaryResponse(summary, tokensUsed);
    }

    private String truncateContent(String content) {
        // Limit to ~4000 tokens worth of text (~3000 words)
        // GPT-4 has 8k context window, leave room for prompt + response
        int maxChars = 15000;
        return content.length() &amp;gt; maxChars 
            ? content.substring(0, maxChars) + &quot;...&quot;
            : content;
    }

    private int calculateMaxTokens(SummaryLength length) {
        // Rough estimate: each sentence needs ~20-30 tokens
        return length.getSentences() * 30 + 50; // +50 for buffer
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Truncate content&lt;/strong&gt;: GPT-4 has an 8k token limit (~6k words). We truncate long articles.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic token limits&lt;/strong&gt;: Different summary lengths need different token budgets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Track metadata&lt;/strong&gt;: Response includes token usage for cost monitoring.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Error Handling&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.example.aisummary.exception;

public class AiServiceException extends RuntimeException {
    public AiServiceException(String message, Throwable cause) {
        super(message, cause);
    }
}

@ControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(AiServiceException.class)
    public ResponseEntity&amp;lt;ErrorResponse&amp;gt; handleAiServiceException(AiServiceException ex) {
        log.error(&quot;AI service error&quot;, ex);
        return ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(new ErrorResponse(
                &quot;AI service temporarily unavailable&quot;,
                &quot;Please try again later&quot;
            ));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity&amp;lt;ErrorResponse&amp;gt; handleValidationException(IllegalArgumentException ex) {
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse(
                &quot;Invalid request&quot;,
                ex.getMessage()
            ));
    }

    record ErrorResponse(String error, String message) {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And update the service to use it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public SummaryResponse summarize(SummaryRequest request) {
    try {
        // ... existing code ...
    } catch (Exception e) {
        throw new AiServiceException(
            &quot;Failed to generate summary for: &quot; + request.article().title(),
            e
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;REST Controller&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.example.aisummary.controller;

import com.example.aisummary.model.*;
import com.example.aisummary.service.ArticleSummarizerService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(&quot;/api/v1/summaries&quot;)
@Tag(name = &quot;Article Summarization&quot;, description = &quot;AI-powered article summarization&quot;)
public class SummaryController {

    private final ArticleSummarizerService summarizerService;

    public SummaryController(ArticleSummarizerService summarizerService) {
        this.summarizerService = summarizerService;
    }

    @PostMapping
    @Operation(
        summary = &quot;Summarize an article&quot;,
        description = &quot;Generate an AI-powered summary of the provided article&quot;
    )
    public ResponseEntity&amp;lt;SummaryResponse&amp;gt; summarizeArticle(
            @Valid @RequestBody SummaryRequest request) {
        
        SummaryResponse response = summarizerService.summarize(request);
        return ResponseEntity.ok(response);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;p&gt;Start your application:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mvn spring-boot:run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then make a request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST http://localhost:8080/api/v1/summaries \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{
    &quot;article&quot;: {
      &quot;title&quot;: &quot;The Future of Java&quot;,
      &quot;content&quot;: &quot;Java has been around for nearly 30 years, and it is not going anywhere. With modern features like records, pattern matching, and virtual threads, Java is faster and more expressive than ever. The JVM ecosystem continues to innovate, with frameworks like Spring, Quarkus, and Micronaut pushing boundaries...&quot;
    },
    &quot;length&quot;: &quot;SHORT&quot;
  }&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;summary&quot;: &quot;Java remains relevant after 30 years with modern features like records and virtual threads. The JVM ecosystem continues to thrive with innovative frameworks. Java&apos;s combination of performance and expressiveness ensures its future.&quot;,
  &quot;tokensUsed&quot;: 127
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get a clean three-sentence summary as requested.&lt;/p&gt;
&lt;h2&gt;Production Patterns&lt;/h2&gt;
&lt;h3&gt;Streaming for Long Responses&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(value = &quot;/stream&quot;, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux&amp;lt;String&amp;gt; streamSummary(@Valid @RequestBody SummaryRequest request) {
    
    Prompt prompt = buildPrompt(request);
    
    // Stream tokens as they arrive
    return chatClient.stream(prompt)
        .map(response -&amp;gt; response.getResult().getOutput().getContent());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Users see progress as the AI generates text.&lt;/p&gt;
&lt;h3&gt;Rate Limiting&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class RateLimiter {
    
    private final Bucket bucket;

    public RateLimiter(@Value(&quot;${app.ai.rate-limit.requests-per-minute}&quot;) int requestsPerMinute) {
        Bandwidth limit = Bandwidth.classic(
            requestsPerMinute,
            Refill.intervally(requestsPerMinute, Duration.ofMinutes(1))
        );
        this.bucket = Bucket.builder().addLimit(limit).build();
    }

    public void checkLimit() {
        if (!bucket.tryConsume(1)) {
            throw new RateLimitExceededException(&quot;Too many requests&quot;);
        }
    }
}

// In service
public SummaryResponse summarize(SummaryRequest request) {
    rateLimiter.checkLimit();
    // ... rest of implementation ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Monitoring and Caching&lt;/h3&gt;
&lt;p&gt;Track usage with Micrometer metrics and cache identical requests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class AiMetrics {
    
    private final MeterRegistry registry;
    private final Counter requestCounter;
    private final Timer responseTimer;
    private final Counter tokensCounter;

    public AiMetrics(MeterRegistry registry) {
        this.registry = registry;
        this.requestCounter = Counter.builder(&quot;ai.requests&quot;)
            .description(&quot;Total AI requests&quot;)
            .register(registry);
        this.responseTimer = Timer.builder(&quot;ai.response.time&quot;)
            .description(&quot;AI response time&quot;)
            .register(registry);
        this.tokensCounter = Counter.builder(&quot;ai.tokens.used&quot;)
            .description(&quot;Total tokens consumed&quot;)
            .register(registry);
    }

    public void recordRequest() {
        requestCounter.increment();
    }

    public void recordResponse(long milliseconds, int tokens) {
        responseTimer.record(milliseconds, TimeUnit.MILLISECONDS);
        tokensCounter.increment(tokens);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class CachedSummarizerService {
    
    @Cacheable(value = &quot;summaries&quot;, key = &quot;#request&quot;)
    public SummaryResponse summarize(SummaryRequest request) {
        return summarizerService.summarize(request);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use Redis for distributed caching in production.&lt;/p&gt;
&lt;h2&gt;Switching AI Providers&lt;/h2&gt;
&lt;p&gt;Spring AI supports multiple providers. Switching is easy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# OpenAI
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}

# Or Azure OpenAI
spring:
  ai:
    azure:
      openai:
        api-key: ${AZURE_OPENAI_KEY}
        endpoint: ${AZURE_OPENAI_ENDPOINT}

# Or Anthropic (Claude)
spring:
  ai:
    anthropic:
      api-key: ${ANTHROPIC_API_KEY}

# Or local models with Ollama
spring:
  ai:
    ollama:
      base-url: http://localhost:11434
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ChatClient&lt;/code&gt; interface stays the same regardless of provider. Great for testing with cheaper models or avoiding vendor lock-in.&lt;/p&gt;
&lt;h2&gt;Common Mistakes&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ignoring token limits&lt;/strong&gt;: Know your model&apos;s context window. Truncate or chunk content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrong temperature&lt;/strong&gt;: Use 0.0-0.3 for facts, 0.7-1.0 for creative content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No retry logic&lt;/strong&gt;: Add &lt;code&gt;@Retryable&lt;/code&gt; with exponential backoff.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exposing raw responses&lt;/strong&gt;: Validate AI output before returning to users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardcoded prompts&lt;/strong&gt;: Store prompts in config so you can update without redeploying.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Where This Makes Sense&lt;/h2&gt;
&lt;p&gt;AI works well for content generation (summaries, descriptions, emails), analysis (sentiment, classification), conversational interfaces (support bots, Q&amp;amp;A), and code assistance (reviews, documentation).&lt;/p&gt;
&lt;p&gt;The key is augmentation, not replacement. AI assists; it doesn&apos;t replace humans.&lt;/p&gt;
&lt;h2&gt;Performance Tips&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Batch requests when possible&lt;/li&gt;
&lt;li&gt;Use GPT-3.5 for simple tasks (10x cheaper, 2x faster)&lt;/li&gt;
&lt;li&gt;Process AI calls asynchronously with &lt;code&gt;@Async&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use local models (Ollama) for development&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;Spring AI integrates cleanly with the Spring ecosystem. The code we built is production-ready—add authentication, persistence, and deploy it.&lt;/p&gt;
&lt;p&gt;If you&apos;re wondering how to add AI without rewriting everything in Python, this is your path. Spring AI meets you where you are.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;: &lt;a href=&quot;https://docs.spring.io/spring-ai/reference/&quot;&gt;Spring AI Docs&lt;/a&gt; • &lt;a href=&quot;https://platform.openai.com/docs/api-reference&quot;&gt;OpenAI API Reference&lt;/a&gt; • &lt;a href=&quot;https://www.promptingguide.ai/&quot;&gt;Prompt Engineering Guide&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>SDD with Spring boot</title><link>https://pixel-pistons.vercel.app/posts/spec-driven-spring-boot/</link><guid isPermaLink="true">https://pixel-pistons.vercel.app/posts/spec-driven-spring-boot/</guid><description>Spec Driven Development with spring boot</description><pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Spring Boot + Spec Driven Development&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;Moving from &lt;strong&gt;code first&lt;/strong&gt; development to &lt;strong&gt;specification first&lt;/strong&gt; development with Spring Boot and AI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;For decades, software development looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Requirements
     ↓
 Developer writes code
     ↓
 Tests
     ↓
 Documentation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately, the requirements were often forgotten after implementation.
The source of truth eventually became the &lt;strong&gt;code&lt;/strong&gt;, not the business requirements.
With modern AI assistants (Claude, GPT-5.x, Copilot etc.), the workflow can fundamentally change.&lt;/p&gt;
&lt;p&gt;Instead of asking:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;How do I write this code?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;we ask&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;How do I specify exactly what the software should do?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the main idea behind &lt;strong&gt;Spec Driven Development (SDD)&lt;/strong&gt; discussed by Simon Martinelli in the Spring Office Hours podcast. &lt;a href=&quot;https://spring.io/blog/2026/05/04/spring-office-hours-podcast-S5E14&quot;&gt;podcast&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The specification becomes the single source of truth.
Code becomes an implementation artifact that can be generated, verified, regenerated, and tested.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Why Spec Driven Development?&lt;/h1&gt;
&lt;p&gt;Traditional development usually evolves like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Business Requirement
↓
Developer interpretation
↓
Implementation
↓
Tests
↓
Documentation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that the original requirement is lost.&lt;/p&gt;
&lt;p&gt;In Spec Driven Development:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Business Requirement
↓
Specification
↓
AI / Developer
↓
Generated Code
↓
Generated Tests
↓
Verification
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The specification never disappears.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Core Principles&lt;/h1&gt;
&lt;h2&gt;1. The specification is the source of truth&lt;/h2&gt;
&lt;p&gt;Instead of saying&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Create a UserService&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;we describe&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The system shall allow a user to register.
Email must be unique.
Password must contain:

- uppercase
- lowercase
- digit
- minimum length 10

Duplicate email returns HTTP 409.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice there is zero Java code.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Specifications are executable&lt;/h2&gt;
&lt;p&gt;A specification should answer&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What inputs exist?&lt;/li&gt;
&lt;li&gt;What outputs exist?&lt;/li&gt;
&lt;li&gt;Validation rules&lt;/li&gt;
&lt;li&gt;Error cases&lt;/li&gt;
&lt;li&gt;Security&lt;/li&gt;
&lt;li&gt;Constraints&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The implementation should satisfy the specification.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. AI writes code&lt;/h2&gt;
&lt;p&gt;Developers review.&lt;/p&gt;
&lt;p&gt;AI generates.&lt;/p&gt;
&lt;p&gt;Developers validate.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Project Structure&lt;/h1&gt;
&lt;p&gt;A Spring Boot project might look like this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;specs/
    user-registration.md
    login.md
    order.md

src/
    main/
    test/

prompts/
    generate-controller.md
    generate-tests.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of hundreds of Jira tickets becoming stale,
the specifications stay versioned with the code.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Example Specification&lt;/h1&gt;
&lt;p&gt;File&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;specs/user-registration.md
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# User Registration

## Endpoint

POST /api/users

## Request

{
  &quot;email&quot;: &quot;john@example.com&quot;,
  &quot;password&quot;: &quot;Password123&quot;
}

## Validation

Email

- required
- valid email
- unique

Password

- minimum length 10
- uppercase required
- lowercase required
- digit required

## Success

HTTP 201

{
   &quot;id&quot;: UUID,
   &quot;email&quot;: &quot;john@example.com&quot;
}

## Errors

400 Invalid request

409 Duplicate email
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No implementation.&lt;/p&gt;
&lt;p&gt;Only behavior.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;From Spec to Spring Boot&lt;/h1&gt;
&lt;p&gt;An AI assistant can generate&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Controller
↓
DTO
↓
Validation
↓
Service
↓
Repository
↓
Tests
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;Generated DTO&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;public record UserRegistrationRequest(

    @Email
    @NotBlank
    String email,

    @NotBlank
    @Pattern(
        regexp = &quot;^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d).{10,}$&quot;
    )
    String password

) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice how validation rules came directly from the specification.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Generated Controller&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/users&quot;)
public class UserController {

    private final UserService service;

    public UserController(UserService service) {
        this.service = service;
    }

    @PostMapping
    public ResponseEntity&amp;lt;UserResponse&amp;gt; register(
            @Valid
            @RequestBody UserRegistrationRequest request
    ) {

        UserResponse response =
                service.register(request);

        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(response);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing magical.&lt;/p&gt;
&lt;p&gt;The controller simply reflects the specification.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Service Layer&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService {

    private final UserRepository repository;

    public UserResponse register(UserRegistrationRequest request) {

        if(repository.existsByEmail(request.email())) {
            throw new DuplicateEmailException();
        }

        User entity = new User(
                UUID.randomUUID(),
                request.email(),
                passwordEncoder.encode(request.password())
        );

        repository.save(entity);

        return UserResponse.from(entity);

    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again,
Business rules came from the specification.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;OpenAPI First&lt;/h1&gt;
&lt;p&gt;Spec Driven Development fits naturally with OpenAPI.&lt;/p&gt;
&lt;p&gt;Example&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;paths:
  /users:
    post:
      summary: Register user
      requestBody:
        required: true
      responses:
        &quot;201&quot;:
          description: User created
        &quot;409&quot;:
          description: Duplicate email
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From this specification you can generate&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controllers&lt;/li&gt;
&lt;li&gt;DTOs&lt;/li&gt;
&lt;li&gt;Clients&lt;/li&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;using tools like OpenAPI Generator.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Testing from Specifications&lt;/h1&gt;
&lt;p&gt;Instead of manually writing tests, AI can derive them.&lt;/p&gt;
&lt;p&gt;Example&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
void duplicateEmailReturns409() {

    given(repository.existsByEmail(any()))
            .willReturn(true);

    mockMvc.perform(post(&quot;/api/users&quot;)
            .contentType(APPLICATION_JSON)
            .content(json))

            .andExpect(status().isConflict());

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The specification explicitly required HTTP 409.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Example Prompt&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;Using the specification below,
generate:

- DTO
- Controller
- Service
- Unit tests
- Integration tests

Requirements:

Use Spring Boot 4

Use Jakarta Validation

Use Problem Details

Do not invent business rules.

Only implement what is specified.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;AI Workflow&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;Business
↓
Specification
↓
Git
↓
AI
↓
Generated Code
↓
Developer Review
↓
Tests
↓
Production
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that AI is not replacing developers.
It is replacing repetitive implementation work.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Keeping Specifications Close to Code&lt;/h1&gt;
&lt;p&gt;A practical repository might look like this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;order-service/
    specs/
        create-order.md
        cancel-order.md
        payment.md
    src/
    prompts/
    docs/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each pull request updates&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;specification&lt;/li&gt;
&lt;li&gt;implementation&lt;/li&gt;
&lt;li&gt;tests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;together.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Example Evolution&lt;/h1&gt;
&lt;h2&gt;Version 1&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Password minimum length

10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Later..&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Password minimum length

12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI regenerates&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;validation&lt;/li&gt;
&lt;li&gt;tests&lt;/li&gt;
&lt;li&gt;documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;without developers hunting through dozens of files.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;What Should Developers Still Do?&lt;/h1&gt;
&lt;p&gt;Spec Driven Development does &lt;strong&gt;not&lt;/strong&gt; eliminate engineering.&lt;/p&gt;
&lt;p&gt;Developers still make architectural decisions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Domain modeling&lt;/li&gt;
&lt;li&gt;Security&lt;/li&gt;
&lt;li&gt;Transactions&lt;/li&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;Distributed systems&lt;/li&gt;
&lt;li&gt;Observability&lt;/li&gt;
&lt;li&gt;Monitoring&lt;/li&gt;
&lt;li&gt;Database design&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI accelerates implementation.
Engineers remain responsible for correctness.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Best Practices with Spring Boot&lt;/h1&gt;
&lt;h2&gt;Keep specifications small&lt;/h2&gt;
&lt;p&gt;One specification per use case.&lt;/p&gt;
&lt;p&gt;Good:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Register User
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Entire User Management Module
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Focus on behavior&lt;/h2&gt;
&lt;p&gt;Good&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Return HTTP 404 if user not found.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bad&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Use Optional.orElseThrow(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Specifications should never prescribe implementation.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Define edge cases&lt;/h2&gt;
&lt;p&gt;Always specify&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;invalid input&lt;/li&gt;
&lt;li&gt;duplicate data&lt;/li&gt;
&lt;li&gt;unauthorized access&lt;/li&gt;
&lt;li&gt;concurrency&lt;/li&gt;
&lt;li&gt;expected HTTP codes&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Let AI generate boilerplate&lt;/h2&gt;
&lt;p&gt;Spring Boot contains plenty of repetitive code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DTOs&lt;/li&gt;
&lt;li&gt;Controllers&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;Mapping&lt;/li&gt;
&lt;li&gt;Tests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are ideal candidates for AI generation.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Advantages&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Better communication with stakeholders&lt;/li&gt;
&lt;li&gt;Requirements remain the source of truth&lt;/li&gt;
&lt;li&gt;Easier onboarding&lt;/li&gt;
&lt;li&gt;Better AI-generated code&lt;/li&gt;
&lt;li&gt;Consistent APIs&lt;/li&gt;
&lt;li&gt;Easier test generation&lt;/li&gt;
&lt;li&gt;Less duplicated documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Challenges&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Writing good specifications requires practice&lt;/li&gt;
&lt;li&gt;Ambiguous requirements still produce ambiguous code&lt;/li&gt;
&lt;li&gt;AI output must always be reviewed&lt;/li&gt;
&lt;li&gt;Architecture cannot be fully automated&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;A Typical Development Cycle&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;1. Write specification
↓
2. Review with stakeholders
↓
3. Commit specification
↓
4. Generate implementation
↓
5. Review generated code
↓
6. Execute tests
↓
7. Deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Spec Driven Development shifts the primary artifact of software engineering from &lt;strong&gt;code&lt;/strong&gt; to &lt;strong&gt;specifications&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For Spring Boot teams, this approach works particularly well because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring&apos;s conventions make generated code predictable.&lt;/li&gt;
&lt;li&gt;Validation rules map naturally from specifications.&lt;/li&gt;
&lt;li&gt;OpenAPI integrates seamlessly with REST APIs.&lt;/li&gt;
&lt;li&gt;AI assistants excel at generating controllers, DTOs, services, and tests from well defined requirements.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key insight is simple:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Code changes frequently. Requirements should not.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When specifications become the single source of truth, Spring Boot becomes the execution platform, AI becomes the implementation assistant, and developers focus on solving business problems rather than writing repetitive boilerplate.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Further Reading&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Spring Office Hours Podcast S5E14: Spec Driven Development with Simon Martinelli&lt;/li&gt;
&lt;li&gt;Spring Boot Reference Documentation&lt;/li&gt;
&lt;li&gt;OpenAPI Specification&lt;/li&gt;
&lt;li&gt;Spring REST Docs&lt;/li&gt;
&lt;li&gt;Spring AI&lt;/li&gt;
&lt;li&gt;OpenAPI Generator&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>