Acceptance Criteria
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
| Letter | Attribute | Description | Example |
|---|---|---|---|
| I | Independent | Story can be developed in any order | Can implement “Apply discount” without “View cart” |
| N | Negotiable | Details can be discussed | Discount calculation method is flexible |
| V | Valuable | Delivers value to user/business | Discounts increase conversion rate |
| E | Estimable | Team can size the story | Roughly 3-5 days of work |
| S | Small | Can be completed in one sprint | Complete in 1 iteration |
| T | Testable | Can verify completion | Can 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
- User Stories Applied (Mike Cohn)
- Specification by Example (Gojko Adzic)
- INVEST in Good User Stories
- Writing Great Acceptance Criteria