Skip to content
Home / Skills / Bdd / Acceptance Criteria
BD

Acceptance Criteria

Bdd requirements v1.0.0

Acceptance Criteria

Overview

Acceptance criteria define the boundaries and expectations of a user story, specifying what must be true for the story to be considered complete. Written before development begins, acceptance criteria serve as the contract between business stakeholders and the development team. They provide concrete, testable conditions that eliminate ambiguity and enable teams to know when they’re done. When combined with BDD, acceptance criteria transform into executable Gherkin scenarios that validate the software automatically.

Well-written acceptance criteria bridge the gap between high-level requirements and detailed implementation, providing clarity without prescribing solutions. They focus on what success looks like from the user’s perspective, enabling developers to design appropriate solutions while ensuring business needs are met.


Key Concepts

Acceptance Criteria vs User Stories

┌─────────────────────────────────────────────────────────┐
│         User Story vs Acceptance Criteria               │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  USER STORY (What & Why)                                │
│  As a [role]                                            │
│  I want [feature]                                       │
│  So that [benefit]                                      │
│                                                         │
│         ↓                                               │
│                                                         │
│  ACCEPTANCE CRITERIA (How we know it works)             │
│  ✓ Scenario 1: Normal flow                             │
│  ✓ Scenario 2: Edge case                               │
│  ✓ Scenario 3: Error condition                         │
│                                                         │
└─────────────────────────────────────────────────────────┘

INVEST Criteria for User Stories

LetterAttributeDescriptionExample
IIndependentStory can be developed in any orderCan implement “Apply discount” without “View cart”
NNegotiableDetails can be discussedDiscount calculation method is flexible
VValuableDelivers value to user/businessDiscounts increase conversion rate
EEstimableTeam can size the storyRoughly 3-5 days of work
SSmallCan be completed in one sprintComplete in 1 iteration
TTestableCan verify completionCan test discount calculation

Formats for Acceptance Criteria

Format 1: Scenario-Based (BDD Style)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Given [precondition]
When [action]
Then [expected result]

Format 2: Rule-Based (Checklist)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ System must validate email format
✓ System must send confirmation email
✓ User must be redirected to dashboard

Format 3: Example-Based
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Input: Order total $100, Discount code "SAVE20"
Output: Final total $80, Discount applied

Format 4: Specification by Example
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Example 1: Valid discount
Example 2: Expired discount
Example 3: Invalid discount code

Acceptance Criteria Properties

Good Acceptance Criteria:

  • Testable (can verify objectively)
  • Clear (no ambiguity)
  • Concise (brief and focused)
  • User-focused (business language)
  • Achievable (technically feasible)
  • Complete (covers happy path + edge cases)

Bad Acceptance Criteria:

  • Vague (“should work well”)
  • Technical (“use Redis for caching”)
  • Implementation-focused (“click button with ID submit-btn”)
  • Too broad (“system is secure”)
  • Not testable (“code is clean”)

Coverage Matrix

What to Cover in Acceptance Criteria:

✓ Happy Path       - Normal, expected flow
✓ Edge Cases       - Boundaries, limits
✓ Error Conditions - Invalid input, failures
✓ Business Rules   - Constraints, validations
✓ Non-Functionals  - Performance, usability
✗ Implementation   - Technology choices
✗ Design Details   - UI specifics (unless critical)

Best Practices

1. Write Acceptance Criteria Before Implementation

Define success criteria during story refinement, not after coding starts. Prevents scope creep and misunderstandings.

2. Make Criteria Observable and Testable

Every criterion should be objectively verifiable. Avoid subjective statements like “fast” or “user-friendly.”

3. Focus on Behavior, Not Implementation

Describe what the system should do, not how it should do it. Leave implementation details to developers.

4. Include Happy Path and Edge Cases

Cover the normal flow, boundary conditions, and error scenarios. Don’t assume only success cases.

5. Use Examples to Clarify

Concrete examples eliminate ambiguity. “10% discount on orders over $100” is clearer than “discount for large orders.”


Code Examples

Example 1: Well-Written Acceptance Criteria

User Story:
As a customer
I want to apply discount codes to my order
So that I can save money on my purchase

# ✅ GOOD - Scenario-based acceptance criteria

Acceptance Criteria:

1. Apply valid percentage discount
   Given I have an order totaling $100.00
   When I apply discount code "SAVE20"
   Then my order total should be $80.00
   And the discount should be shown as "-$20.00"

2. Apply valid fixed amount discount
   Given I have an order totaling $100.00
   When I apply discount code "FLAT10"
   Then my order total should be $90.00
   And the discount should be shown as "-$10.00"

3. Reject expired discount code
   Given discount code "EXPIRED" expired yesterday
   When I apply discount code "EXPIRED"
   Then I should see error "This discount code has expired"
   And my order total should remain $100.00

4. Reject invalid discount code
   Given I have an order totaling $100.00
   When I apply discount code "INVALID"
   Then I should see error "Invalid discount code"
   And my order total should remain $100.00

5. Only one discount per order
   Given I have applied discount code "SAVE20"
   And my order total is now $80.00
   When I attempt to apply discount code "FLAT10"
   Then I should see error "Only one discount code allowed per order"
   And my order total should remain $80.00

6. Discount codes are case-insensitive
   Given I have an order totaling $100.00
   When I apply discount code "save20" (lowercase)
   Then my order total should be $80.00

7. Minimum order value requirement
   Given I have an order totaling $25.00
   When I apply discount code "SAVE20" (requires $50 minimum)
   Then I should see error "Minimum order value of $50 required"
   And my order total should remain $25.00

# ❌ BAD - Vague, not testable
# Acceptance Criteria:
# - System should handle discount codes
# - Discount codes should work correctly
# - Error handling should be good
# - Performance should be acceptable

Example 2: From Story to Executable Criteria

// User Story
// As a bank customer
// I want to transfer money between my accounts
// So that I can manage my finances

// ✅ GOOD - Clear acceptance criteria

/**
 * Acceptance Criteria:
 * 
 * AC1: Successful transfer between accounts
 * Given I have $1000 in checking account
 * And I have $500 in savings account
 * When I transfer $200 from checking to savings
 * Then checking account should have $800
 * And savings account should have $700
 * And I should see confirmation "Transfer successful"
 * 
 * AC2: Insufficient funds
 * Given I have $100 in checking account
 * When I transfer $200 from checking to savings
 * Then I should see error "Insufficient funds"
 * And checking account should remain $100
 * And no transfer should be recorded
 * 
 * AC3: Invalid amount
 * When I attempt to transfer $0
 * Then I should see error "Transfer amount must be positive"
 * 
 * AC4: Same source and destination
 * When I attempt to transfer from checking to checking
 * Then I should see error "Source and destination must be different"
 * 
 * AC5: Transfer limits
 * Given I have $10000 in checking account
 * When I attempt to transfer $6000
 * Then I should see error "Daily transfer limit of $5000 exceeded"
 */

// Implementation - these ACs drive the service design
public class AccountTransferService {
    
    public TransferResult transfer(
        String fromAccountId,
        String toAccountId,
        Money amount
    ) {
        // AC3: Validate amount
        if (amount.isZeroOrNegative()) {
            return TransferResult.error("Transfer amount must be positive");
        }
        
        // AC4: Validate different accounts
        if (fromAccountId.equals(toAccountId)) {
            return TransferResult.error("Source and destination must be different");
        }
        
        Account fromAccount = accountRepository.findById(fromAccountId)
            .orElseThrow(() -> new AccountNotFoundException(fromAccountId));
        Account toAccount = accountRepository.findById(toAccountId)
            .orElseThrow(() -> new AccountNotFoundException(toAccountId));
        
        // AC2: Check sufficient funds
        if (fromAccount.getBalance().isLessThan(amount)) {
            return TransferResult.error("Insufficient funds");
        }
        
        // AC5: Check transfer limits
        if (amount.isGreaterThan(DAILY_TRANSFER_LIMIT)) {
            return TransferResult.error(
                "Daily transfer limit of " + DAILY_TRANSFER_LIMIT + " exceeded"
            );
        }
        
        // AC1: Perform transfer
        fromAccount.debit(amount);
        toAccount.credit(amount);
        
        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
        
        return TransferResult.success("Transfer successful");
    }
}

// ✅ Each AC maps to a test
@Test
void shouldTransferBetweenAccounts_AC1() {
    // Tests AC1: Successful transfer
}

@Test
void shouldRejectTransfer_whenInsufficientFunds_AC2() {
    // Tests AC2: Insufficient funds
}

@Test
void shouldRejectTransfer_whenAmountInvalid_AC3() {
    // Tests AC3: Invalid amount
}

Example 3: Rule-Based vs Scenario-Based Criteria

User Story:
As a user
I want to register for an account
So that I can access the platform

# ❌ PROBLEMATIC - Rule-based (lacks context)
Acceptance Criteria:
- Email must be unique
- Password must be at least 8 characters
- Password must contain uppercase letter
- Password must contain lowercase letter
- Password must contain digit
- System must send confirmation email
- User must confirm email within 24 hours

# ✅ BETTER - Scenario-based (provides context)
Acceptance Criteria:

Scenario: Successfully register with valid details
  Given no account exists for "john@example.com"
  When I register with:
    | email              | john@example.com |
    | password           | SecurePass1      |
    | confirmPassword    | SecurePass1      |
  Then my account should be created
  And I should receive confirmation email
  And I should be redirected to "confirm email" page

Scenario: Cannot register with existing email
  Given an account exists for "john@example.com"
  When I attempt to register with email "john@example.com"
  Then I should see error "Email already registered"
  And no new account should be created

Scenario Outline: Reject weak passwords
  When I register with password "<password>"
  Then I should see error "<error>"
  
  Examples:
    | password    | error                                    |
    | short       | Password must be at least 8 characters   |
    | NoDigits    | Password must contain at least 1 digit   |
    | no-upper    | Password must contain uppercase letter   |
    | NO-LOWER    | Password must contain lowercase letter   |

Scenario: Password confirmation must match
  When I register with password "SecurePass1"
  And password confirmation "DifferentPass1"
  Then I should see error "Passwords do not match"

Scenario: Email confirmation expires after 24 hours
  Given I registered account 25 hours ago
  And I have not confirmed my email
  When I attempt to login
  Then I should see message "Please request new confirmation email"
  And I should not be able to login

# ✅ Benefits of scenario-based:
# - Provides context (Given)
# - Shows concrete examples
# - Easier to convert to automated tests
# - More readable by non-technical stakeholders

Example 4: Acceptance Criteria with Non-Functionals

User Story:
As a customer
I want to search for products
So that I can find items to purchase

# ✅ GOOD - Includes functional and non-functional criteria

Functional Acceptance Criteria:

1. Search by keyword
   Given the catalog contains:
     | name           | category    |
     | Widget Pro     | Electronics |
     | Widget Basic   | Electronics |
     | Gadget Supreme | Electronics |
   When I search for "Widget"
   Then I should see 2 results
   And results should include "Widget Pro"
   And results should include "Widget Basic"

2. Filter by category
   When I search for "Electronics"
   Then I should see 3 results

3. Handle no results
   When I search for "NonexistentProduct"
   Then I should see message "No products found"
   And I should see suggestions for alternative searches

4. Search with special characters
   When I search for "widget & gadget"
   Then the search should handle special characters
   And return relevant results

Non-Functional Acceptance Criteria:

5. Performance
   Given the catalog contains 10,000 products
   When I perform a search
   Then results should return within 500ms
   And the page should remain responsive

6. Relevance
   Given I search for "wireless mouse"
   Then results should be ordered by relevance
   And exact matches should appear first
   And partial matches should appear after

7. Pagination
   Given search returns 100 results
   Then I should see first 20 results
   And pagination controls should be displayed
   And I can navigate to next page

8. Accessibility
   When I use keyboard navigation
   Then I can search without using mouse
   And search results are screen reader compatible

# ✅ Both functional and non-functional criteria
# ✅ Specific, measurable (500ms, 20 results)
# ✅ Testable through automated tests

Example 5: Anti-Patterns in Acceptance Criteria

# ❌ ANTI-PATTERN 1: Too implementation-focused

BAD:
Acceptance Criteria:
- Use PostgreSQL for data storage
- Implement using Spring Boot
- Use Redis for caching discount codes
- Store discount in discount_codes table
- Call DiscountService.applyDiscount() method

GOOD:
Acceptance Criteria:
- System should persist discount codes
- Discount application should be performant (< 100ms)
- Discount codes should be available across sessions

# ❌ ANTI-PATTERN 2: Not testable

BAD:
Acceptance Criteria:
- Code should be clean and maintainable
- System should be user-friendly
- Performance should be good
- Error messages should be helpful

GOOD:
Acceptance Criteria:
- Error message should specify what went wrong
  Example: "Invalid discount code" instead of "Error occurred"
- Search results should return within 500ms for 95% of requests
- UI should follow WCAG 2.1 Level AA guidelines

# ❌ ANTI-PATTERN 3: Too vague

BAD:
Acceptance Criteria:
- Discount codes should work
- System should handle errors
- Users should be able to checkout

GOOD:
Scenario: Apply valid discount code
  Given order total is $100
  When I apply code "SAVE20"
  Then total should be $80

Scenario: Handle expired discount code
  Given code "EXPIRED" expired yesterday
  When I apply code "EXPIRED"
  Then I see error "This discount code has expired"

# ❌ ANTI-PATTERN 4: Missing edge cases

BAD:
Acceptance Criteria:
Given I have an order
When I apply discount code
Then discount is applied

MISSING:
- What if discount code is invalid?
- What if discount code is expired?
- What if order total is $0?
- What if discount exceeds order total?
- What if code already applied?

GOOD:
Include scenarios for:
✓ Valid discount (happy path)
✓ Invalid/expired code (error cases)
✓ Multiple discounts (business rule)
✓ Edge cases (zero total, discount > total)

# ❌ ANTI-PATTERN 5: Too granular (micro-managing)

BAD:
Acceptance Criteria:
- When user clicks "Apply" button
- System validates input using regex pattern [A-Z0-9]{6}
- System queries database using SELECT * FROM discount_codes WHERE code = ?
- If found, calculate discount using formula: total * (percentage / 100)
- Update order_discount column in orders table
- Refresh page displaying new total in <div id="total">

GOOD:
Acceptance Criteria:
When I apply valid discount code
Then discount is calculated and applied to order
And updated total is displayed

# Let implementation details be decided by developers

Anti-Patterns

❌ Writing Acceptance Criteria After Implementation

// WRONG workflow:
1. Developer implements feature
2. QA tests and finds issues
3. Team writes acceptance criteria to match implementation
4. Criteria always pass (confirmation bias)

// Result: Acceptance criteria serve no purpose

// ✅ CORRECT workflow:
1. Team writes acceptance criteria during refinement
2. Criteria reviewed by BA, developer, tester (Three Amigos)
3. Developer implements to meet criteria
4. Tests verify criteria are met

// Result: Criteria drive development, catch issues

❌ Technical Acceptance Criteria

// ❌ BAD - Technical constraints as acceptance criteria
Acceptance Criteria:
- Use microservices architecture
- Implement with Spring Boot 3.x
- Deploy to Kubernetes
- Use OAuth2 for authentication
- Store data in MongoDB

// These are technical decisions, not acceptance criteria

// ✅ GOOD - Business-focused criteria
Acceptance Criteria:
- User can login with email and password
- User session expires after 30 minutes of inactivity
- User can access protected resources after login
- System handles 1000 concurrent users

// Technical decisions support these criteria, not define them

❌ Acceptance Criteria Without Examples

// ❌ BAD - Abstract rules without examples
Acceptance Criteria:
- Discount codes should work properly
- System should validate codes
- Error messages should be clear

// Too vague, open to interpretation

// ✅ GOOD - Concrete examples
Acceptance Criteria:
Example 1: Valid 20% discount code "SAVE20"
  Input: Order total $100, code "SAVE20"
  Output: Total $80, discount shown "-$20"

Example 2: Expired code "SUMMER2023"
  Input: Order total $100, code "SUMMER2023" (expired)
  Output: Error "This discount code has expired"

Example 3: Invalid code format
  Input: Order total $100, code "ABC"
  Output: Error "Invalid discount code"

// Clear, testable, unambiguous

Testing Strategies

Mapping Criteria to Tests

// Each acceptance criterion → Automated test

// AC1: Apply valid percentage discount
@Test
void shouldApplyPercentageDiscount_AC1() {
    Order order = createOrderWithTotal(100.00);
    
    discountService.apply(order, "SAVE20");
    
    assertThat(order.getTotal()).isEqualTo(Money.usd(80.00));
    assertThat(order.getDiscount()).isEqualTo(Money.usd(20.00));
}

// AC2: Reject expired code
@Test
void shouldRejectExpiredCode_AC2() {
    Order order = createOrderWithTotal(100.00);
    DiscountCode expiredCode = createExpiredCode("EXPIRED");
    
    assertThatThrownBy(() -> discountService.apply(order, "EXPIRED"))
        .isInstanceOf(DiscountException.class)
        .hasMessage("This discount code has expired");
    
    assertThat(order.getTotal()).isEqualTo(Money.usd(100.00));
}

// AC3: Only one discount per order
@Test
void shouldRejectMultipleDiscounts_AC3() {
    Order order = createOrderWithTotal(100.00);
    discountService.apply(order, "SAVE20");
    
    assertThatThrownBy(() -> discountService.apply(order, "FLAT10"))
        .isInstanceOf(DiscountException.class)
        .hasMessage("Only one discount code allowed per order");
}

// Traceability: Test name references AC number
// When AC changes, update corresponding test

Acceptance Criteria Review Checklist

Before accepting acceptance criteria, verify:

✓ TESTABLE
  - Can be verified objectively
  - Specific, measurable outcomes
  - Clear pass/fail conditions

✓ CLEAR
  - No ambiguous terms ("fast", "good", "user-friendly")
  - Concrete examples provided
  - Edge cases identified

✓ USER-FOCUSED
  - Written in business language
  - Describes behavior, not implementation
  - Focuses on user value

✓ COMPLETE
  - Happy path covered
  - Error conditions covered
  - Edge cases covered
  - Non-functionals included where relevant

✓ INDEPENDENT
  - Can be verified without other stories
  - No dependencies on unimplemented features

✓ ACHIEVABLE
  - Technically feasible
  - Can be completed in one sprint
  - Team has necessary skills/tools

Definition of Done Integration

Story is Done when:

✓ All acceptance criteria met
✓ Automated tests written for each AC
✓ Tests passing in CI/CD
✓ Code reviewed and approved
✓ Documentation updated
✓ Deployed to staging environment
✓ Product owner acceptance

Example:

User Story: Apply discount codes
✓ AC1: Valid discount applied (test: shouldApplyValidDiscount)
✓ AC2: Invalid code rejected (test: shouldRejectInvalidCode)
✓ AC3: Expired code rejected (test: shouldRejectExpiredCode)
✓ AC4: Only one discount allowed (test: shouldEnforceSingleDiscount)
✓ Code review approved by @senior-dev
✓ Gherkin scenarios passing in Cucumber
✓ Deployed to staging and verified
✓ Product owner signed off

= Story can be closed

References