Phase 4.5: Spec as Source of Truth – Rethinking Development Workflow

After stepping back from the “I will never give up” AI issue, I decided to tackle a more fundamental problem: the sync between spec-story-task-test. The question became: which part should be the authoritative source for requirements, examples, API spec…


This content originally appeared on DEV Community and was authored by Maksim Matlakhov

After stepping back from the "I will never give up" AI issue, I decided to tackle a more fundamental problem: the sync between spec-story-task-test. The question became: which part should be the authoritative source for requirements, examples, API specs, and how do we manage spec updates without creating chaos?

The Current Workflow

From my experience, here's how development typically works:

  1. PO builds a document with requirements (or skips to the next step)
  2. Split to stories: Either common stories or platform-specific stories (backend, web, mobile)
  3. Developers create dev tasks (or work directly under stories)
  4. Implementation begins
  5. Changes happen during development - but requirements/stories don't always get updated
  6. Unsync occurs: If no spec exists, stories (or code) become the source of truth
  7. Change requests create confusion:
    • PO updates spec and creates new stories
    • If spec is outdated, changes apply to irrelevant source
    • If spec gets updated but changes aren't implemented, PO forgets to revert
    • Nobody knows current functionality status when change requests are in progress
    • If there is spec versioning then it adds complexity and mess

My Proposed Solution: Story-First, Spec-Last Workflow

1. Story Creation

  • PO creates a master story with requirements
  • Describes which components need changes: backend, web, mobile
  • Uses structured templates designed for both humans and AI

Example: Master Story for Create Payout

# Story: Create Payout - Submit New Payout Request

## Story Overview
**As a** user  
**I want to** create a new payout request  
**So that** I can receive funds to my account

## User Acceptance Criteria
1. **Given** I have valid request params **When** I submit a payout request with valid details **Then** the payout is created successfully
2. **Given** I provide invalid or missing required fields **When** I submit the payout request **Then** I receive validation error messages
3. **Given** my total payout amount would exceed the allowed limit **When** I submit the payout request **Then** I receive an error about exceeding limits

## Business Rules
- **UserId**: Must be provided and be a valid UUID format
- **Amount**: Must be provided and must be in range of configured individual payout limit
- **Currency**: Must be provided, must be valid ISO code, and must be one of configured allowed currencies
- **User Total Limit**: The sum of all payouts for the user must not exceed configured total limit

## Error Scenarios
- **Missing UserId**: When UserId is not provided → User sees "User ID is required"
- **Invalid UserId**: When UserId is not valid UUID format → User sees "Invalid User ID format"
- **Missing Amount**: When Amount is not provided → User sees "Amount is required"
- **Invalid Amount**: When Amount is out of range → User sees "Amount must be from {min} to {max}"
- **Amount Limit Exceeded**: When Amount exceeds configured individual limit → User sees "Amount exceeds maximum allowed limit: {limit}"
- **Missing Currency**: When Currency is not provided → User sees "Currency is required"
- **Invalid Currency**: When Currency is not valid ISO code → User sees "Invalid currency code"
- **Currency Not Allowed**: When Currency is not in configured allowed list → User sees "Currency not supported, supported currencies: {currencies}"
- **Total Limit Exceeded**: When user's total payouts would exceed configured limit → User sees "Total payout limit {limit} exceeded for your account"

## Configuration Requirements
- **Individual Payout Limit**: Min payout is 0.10 and Max is 30.00
- **User Total Limit**: Maximum total amount a user can have is 120
- **Allowed Currencies**: List of supported ISO currency codes: EUR, USD, GBP

2. Story Breakdown

  • PO creates component-specific sub-stories with help from dev teams
  • Add high level technical details: acceptance criteria, API interfaces, data objects (in JSON)

Example: API Component Sub-Story

# POST /v1/payouts - Create Payout

## Endpoint Definition
**Purpose**: Create new payout request based on user request  
**Method**: POST  
**Path**: /v1/payouts  
**Content-Type**: application/json

## Request Structure

### Request Body: CreatePayoutParamsV1
{
  "userId": "123e4567-e89b-12d3-a456-426614174000",
  "amount": 25.50,
  "currency": "USD"
}

## Input Validation
**Field-level validation** (format, required, type checking):
- **userId**: NotNull, UUID
- **amount**: NotNull, BigDecimal
- **currency**: NotNull, Pattern(ISO_CURRENCY_CODE)

## Business Validation
**Business rules** that apply during creation:

1. **Individual Payout Limit**: Amount must be within configured individual payout limits
    - Test Scenarios:
        - Configuration: minAmount = 0.10, maxAmount = 30.00
        - givenAmount=0.10 → 201 Created
        - givenAmount=30.00 → 201 Created
        - givenAmount=0.09 → 422 AMOUNT_OUT_OF_RANGE
        - givenAmount=30.01 → 422 AMOUNT_OUT_OF_RANGE

2. **User Total Limit**: The sum of all payouts for the user must not exceed configured total limit
    - Test Scenarios:
        - Configuration: userTotalLimit = 120.00
        - userTotal=90.0, givenAmount=25.0 → 201 Created
        - userTotal=0.0, givenAmount=119.99 → 201 Created
        - userTotal=100.0, givenAmount=25.0 → 422 USER_TOTAL_LIMIT_EXCEEDED
        - userTotal=115.0, givenAmount=10.0 → 422 USER_TOTAL_LIMIT_EXCEEDED

3. **Currency Support**: Currency must be one of configured allowed currencies
    - Test Scenarios:
        - Configuration: allowedCurrencies = ["EUR", "USD", "GBP"]
        - givenCurrency=EUR → 201 Created
        - givenCurrency=USD → 201 Created
        - givenCurrency=GBP → 201 Created
        - givenCurrency=JPY → 422 CURRENCY_NOT_SUPPORTED

## Success Response

### 201 Created
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "version": 1,
  "createdAt": "2025-08-20T10:30:00Z",
  "updatedAt": "2025-08-20T10:30:00Z",
  "data": {
    "userId": "123e4567-e89b-12d3-a456-426614174000",
    "amount": 25.50,
    "currency": "USD"
  }
}

## Error Responses

### 422 Unprocessable Entity - Missing UserId
{
  "errors": [
    {
      "code": "REQUIRED_FIELD_MISSING",
      "message": "User ID is required",
      "attributes": {
        "field": "userId"
      }
    }
  ]
}

### 422 Unprocessable Entity - Invalid UserId Format
{
  "errors": [
    {
      "code": "INVALID_FORMAT",
      "message": "Invalid User ID format",
      "attributes": {
        "field": "userId",
        "value": "invalid-uuid"
      }
    }
  ]
}

### 422 Unprocessable Entity - Missing Amount
{
  "errors": [
    {
      "code": "REQUIRED_FIELD_MISSING",
      "message": "Amount is required",
      "attributes": {
        "field": "amount"
      }
    }
  ]
}

### 422 Unprocessable Entity - Invalid Amount Range
{
  "errors": [
    {
      "code": "AMOUNT_OUT_OF_RANGE",
      "message": "Amount must be from {minAmount} to {maxAmount}",
      "attributes": {
        "field": "amount",
        "value": "0.05",
        "minAmount": "{minAmount}",
        "maxAmount": "{maxAmount}"
      }
    }
  ]
}

### 422 Unprocessable Entity - Missing Currency
{
  "errors": [
    {
      "code": "REQUIRED_FIELD_MISSING",
      "message": "Currency is required",
      "attributes": {
        "field": "currency"
      }
    }
  ]
}

### 422 Unprocessable Entity - Invalid Currency Code
{
  "errors": [
    {
      "code": "INVALID_CURRENCY_CODE",
      "message": "Invalid currency code",
      "attributes": {
        "field": "currency",
        "value": "INVALID"
      }
    }
  ]
}

### 422 Unprocessable Entity - Currency Not Supported
{
  "errors": [
    {
      "code": "CURRENCY_NOT_SUPPORTED",
      "message": "Currency not supported, supported currencies: {currencies}",
      "attributes": {
        "field": "currency",
        "value": "JPY",
        "supportedCurrencies": ["{currencies}"]
      }
    }
  ]
}

### 422 Unprocessable Entity - User Total Limit Exceeded
{
  "errors": [
    {
      "code": "USER_TOTAL_LIMIT_EXCEEDED",
      "message": "Total payout limit {limit} exceeded for your account",
      "attributes": {
        "field": "amount",
        "requestedAmount": "25.00",
        "currentTotal": "100.00",
        "totalLimit": "{limit}",
        "wouldExceedBy": "5.00"
      }
    }
  ]
}

## External Service Dependencies
- **Configuration Service**: Used for retrieving payout limits and supported currencies
    - **Timeout**: 2 seconds
    - **Retry**: 3 attempts with exponential backoff
    - **Fallback**: Use cached configuration values if service unavailable

3. Sub-Story Breakdown

  • Developers break down into dev tasks with low-level details
  • Acceptance criteria becomes specific behavior tests in human-readable language (no code logic)

Example: Core Business Logic Task (Center of Hexagon)

# CreatePayoutUseCase - Business Logic Write Operation

## Use Case Definition
**Purpose**: Execute business logic for create payout  
**Entry Point**: `CreatePayoutUseCase.execute(command)`  
**Testing Strategy**: Behavioral black box testing with real business logic

## Required Objects for Implementation

### Command Structure
data class CreatePayoutCommand(
    val userId: UUID,
    val amount: BigDecimal,
    val currency: String,
)

### PayoutErrorCode
AMOUNT_OUT_OF_RANGE("Amount must be from {minAmount} to {maxAmount}"),
USER_TOTAL_LIMIT_EXCEEDED("Total payout limit {limit} exceeded for your account"),
CURRENCY_NOT_SUPPORTED("Currency not supported, supported currencies: {currencies}"),

### Business Logic Components
- `CreatePayoutUseCase` - main use case handler
- `PayoutValidator` - orchestrates validation rules
- `AmountValidator` - validates individual payout limits
- `UserTotalValidator` - validates user total limit
- `CurrencyValidator` - validates currency support

### External Ports
- `PayoutStoragePort` - payout storage operations
- `PayoutConfigPort` - business configuration access

### Domain Object
data class Payout(
    val userId: UUID,
    val amount: BigDecimal,
    val currency: String,
)

## Test Scenarios

### Base Test: `CreatePayoutUseCaseTestBase`
- Abstract class with common setup
- Mock external ports only
    - payoutStoragePort
    - payoutConfigPort
- Mock random generator providers
    - idProvider
    - timeProvider
- Configuration
    - minAmount = 0.10
    - maxAmount = 30.00
    - userTotalLimit = 120.00
    - allowedCurrencies = ["EUR", "USD", "GBP"]
- Valid Command
    - userId = randomUUID
    - amount = randomBigDecimal between 0.10 and 30.00
    - currency = randomString from allowedCurrencies list

### General Tests: `CreatePayoutUseCaseTest`
- Single Test: `should create payout when all business rules satisfied`
    - Given:
        * Valid command
    - When: execute use case with CreatePayoutCommand
    - Then: success result with payout created
    - And: payoutStoragePort.save() called

### Business Individual Payout Limit Tests: `CreatePayoutUseCaseAmountTest`
- Parametrized Test: `should create payout when amount is within limits`
    - Given value source:
        * amount = [0.10, 30.00]
    - When: execute use case with CreatePayoutCommand
    - Then: success result with payout created
    - And: payoutStoragePort.save() called

- Parametrized Test: `should reject payout when amount is out of range`
    - Given value source:
        * amount = [0.09, 30.01]
    - When: execute use case with CreatePayoutCommand
    - Then: throws PayoutValidationException with AMOUNT_OUT_OF_RANGE and attributes [field=amount, value=givenAmount, minAmount=0.10, maxAmount=30.00]
    - And: payoutStoragePort.save never called

### Business User Total Limit Tests: `CreatePayoutUseCaseUserTotalTest`
- Parametrized Test: `should create payout when user total limit not exceeded`
    - Given csv source:
        * userTotal,amount = [90.0,25.0], [0.0,119.99]
    - When: execute use case with CreatePayoutCommand
    - Then: success result with payout created
    - And: payoutStoragePort.save() called

- Parametrized Test: `should reject payout when user total limit exceeded`
    - Given csv source:
        * userTotal,amount,wouldExceedBy = [100.0,25.0,5.00], [115.0,10.0,5.00]
    - When: execute use case with CreatePayoutCommand
    - Then: throws PayoutValidationException with USER_TOTAL_LIMIT_EXCEEDED and attributes [field=amount, requestedAmount=givenAmount, currentTotal=userTotal, totalLimit=120.00, wouldExceedBy=wouldExceedBy]
    - And: payoutStoragePort.save never called

### Business Currency Support Tests: `CreatePayoutUseCaseCurrencyTest`
- Parametrized Test: `should create payout when currency is supported`
    - Given value source:
        * currency = [EUR, USD, GBP]
    - When: execute use case with CreatePayoutCommand
    - Then: success result with payout created
    - And: payoutStoragePort.save() called

- Single Test: `should reject payout when currency is not supported`
    - Given:
        * currency = JPY
    - When: execute use case with CreatePayoutCommand
    - Then: throws PayoutValidationException with CURRENCY_NOT_SUPPORTED and attributes [field=currency, value=JPY, supportedCurrencies=[EUR, USD, GBP]]
    - And: payoutStoragePort.save never called

4. Implementation

  • Create tests in code based on behavior tests from tasks
  • Implement logic without modifying tests
  • Critical rule: If developers realize business requirements are wrong during implementation, fix the master/component specs first - never modify tests in code

Example: Generated Test Code (Red Phase)

class CreatePayoutUseCaseAmountTest : CreatePayoutUseCaseTestBase() {

    @ParameterizedTest
    @ValueSource(doubles = [0.10, 30.00])
    fun `should create payout when amount is within limits`(givenAmount: Double) {
        // Given
        val givenCommand = PayoutMother.createPayoutCommand(
            amount = givenAmount.toBigDecimal()
        )

        // When
        val actualResult = createPayoutUseCase.execute(givenCommand)

        // Then
        shouldBeValid(actualResult)
    }

    @ParameterizedTest
    @ValueSource(doubles = [0.09, 30.01])
    fun `should reject payout when amount is out of range`(givenAmount: Double) {
        // Given
        val givenCommand = PayoutMother.createPayoutCommand(
            amount = givenAmount.toBigDecimal()
        )

        val expectedErrors = listOf(
            ValidationError(
                code = PayoutErrorCode.AMOUNT_OUT_OF_RANGE,
                attributes = mapOf(
                    PayoutValidationField.FIELD to PayoutValidationField.AMOUNT,
                    PayoutValidationField.VALUE to givenAmount,
                    PayoutValidationField.MIN_AMOUNT to configuredAmountRange.from,
                    PayoutValidationField.MAX_AMOUNT to configuredAmountRange.to
                )
            )
        )

        // When & Then
        shouldBeInvalid(givenCommand, expectedErrors)
    }
}

5. Spec Generation

  • After all sub-tasks are finished and feature is released, create detailed spec from stories/sub-stories/dev tasks
  • Create multiple spec levels: master spec, platform-specific specs, dev-specific specs
  • Specs show current production state - they are the source of truth for what's live

6. Change Management

  • Create new story for change requests (don't touch existing specs)
  • Repeat the entire process
  • Merge changes to existing specs after implementation and release
  • This way specs always reflect current production reality

AI Integration Strategy

Template System

Every story/task gets:

  • Structured templates with step-by-step filling guides
  • Guides adapted for both humans and AI
  • Working examples for every pattern AI needs to implement

AI Workflow

  1. Story Generation: Take raw requirements → create stories/sub-stories/tasks/sub-tasks using AI → carefully verify and correct
  2. Test Generation: Once behavior tests and objects are clearly defined on paper → ask AI to convert to code
  3. Spec Generation: After implementation and release → ask AI to transform stories and tasks into specs
  4. Change Integration: For change requests → ask AI to merge changes to existing specs

My Testing Results So Far

What's Working

  • Templates and guides for master story, API story, core hexagon business logic work reasonably well
  • AI generates mostly correct content, though it still invents some details
  • Self-check prompts help - asking AI to revisit generated docs catches some mistakes (but not all)
  • No "never give up" issue because AI isn't writing code that must compile and run

What's Still Problematic

  • Test transformation inconsistency: Same request executed multiple times produces different results
  • AI sometimes:
    • Implements logic instead of keeping it empty (red phase violation)
    • Incorrectly prepares components (use case handlers, validators)
    • Tries to change common code that shouldn't be touched
  • Prompt quality: My current prompts and guides need improvement

The Test Generation Challenge

When converting tasks to tests, AI struggles with maintaining the "red phase" - creating failing tests without implementation. It wants to solve the problem rather than just create the test structure.

Proposed Solutions

1. Better Examples Strategy

  • Provide examples of red phase tests (tests without implementation)
  • Stop providing full examples with implementation
  • Let AI see correct test patterns without working solutions
  • This should help AI understand it needs to create structure, not solve problems. At least I hope so

2. Repository Separation

  • Split current monolith into multiple repositories:
    • Libraries repository
    • Domain-specific repositories
    • API repository
  • Compose monolith from JARs of separate repositories
  • This physically prevents AI from touching unwanted code (it has no access)

3. Enhanced Guidance

  • Create more detailed step-by-step guides for component preparation
  • Add specific examples for each type of component AI needs to create
  • Improve prompt engineering based on observed failure patterns

Key Insights

1. Specs Should Reflect Reality, Not Intentions

Traditional approach of updating specs before implementation creates confusion about what's actually live. Specs should always show current production state.

2. Stories Are Better Change Vehicles Than Specs

Stories naturally capture the "what needs to change" mindset. Specs are better for "what currently exists" documentation.

3. AI Needs Extreme Structure

The more structure and examples provided, the less room for AI creativity and invention. Structure eliminates unwanted problem-solving.

4. Physical Boundaries Beat Prompts

Separating repositories works better than asking AI not to modify certain files. Make it impossible, not just discouraged.

Next Steps for Phase 4.6

The immediate focus will be on solving the two technical challenges:

  1. Repository restructuring: Split the monolith to physically limit AI's modification scope
  2. Red-phase test examples: Create comprehensive examples showing test structure without implementation

These foundational changes should address the core issues preventing reliable AI test generation while maintaining the story-first workflow structure.


This content originally appeared on DEV Community and was authored by Maksim Matlakhov


Print Share Comment Cite Upload Translate Updates
APA

Maksim Matlakhov | Sciencx (2025-08-26T08:50:52+00:00) Phase 4.5: Spec as Source of Truth – Rethinking Development Workflow. Retrieved from https://www.scien.cx/2025/08/26/phase-4-5-spec-as-source-of-truth-rethinking-development-workflow/

MLA
" » Phase 4.5: Spec as Source of Truth – Rethinking Development Workflow." Maksim Matlakhov | Sciencx - Tuesday August 26, 2025, https://www.scien.cx/2025/08/26/phase-4-5-spec-as-source-of-truth-rethinking-development-workflow/
HARVARD
Maksim Matlakhov | Sciencx Tuesday August 26, 2025 » Phase 4.5: Spec as Source of Truth – Rethinking Development Workflow., viewed ,<https://www.scien.cx/2025/08/26/phase-4-5-spec-as-source-of-truth-rethinking-development-workflow/>
VANCOUVER
Maksim Matlakhov | Sciencx - » Phase 4.5: Spec as Source of Truth – Rethinking Development Workflow. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/08/26/phase-4-5-spec-as-source-of-truth-rethinking-development-workflow/
CHICAGO
" » Phase 4.5: Spec as Source of Truth – Rethinking Development Workflow." Maksim Matlakhov | Sciencx - Accessed . https://www.scien.cx/2025/08/26/phase-4-5-spec-as-source-of-truth-rethinking-development-workflow/
IEEE
" » Phase 4.5: Spec as Source of Truth – Rethinking Development Workflow." Maksim Matlakhov | Sciencx [Online]. Available: https://www.scien.cx/2025/08/26/phase-4-5-spec-as-source-of-truth-rethinking-development-workflow/. [Accessed: ]
rf:citation
» Phase 4.5: Spec as Source of Truth – Rethinking Development Workflow | Maksim Matlakhov | Sciencx | https://www.scien.cx/2025/08/26/phase-4-5-spec-as-source-of-truth-rethinking-development-workflow/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.