The Coffee Shop Crisis

A code-dependent life form.

Meet Alex. Three months ago, Alex fulfilled a lifelong dream: opening "Rusty Bean", a cozy coffee shop in downtown Portland. Business is booming! Students camp out with their laptops, seniors meet for their morning ritual, and the loyalty program is attracting regulars.
But there's a problem. A big one.
It's 2 AM, and Alex is staring at the laptop screen in the empty shop. The code that handles discount calculations has become a monster. What started as a simple "if student, then 20% off" has grown into an unmaintainable nightmare.
The Mess
Let's take a look at what Alex is dealing with. The discount calculation system started simple enough, a few if statements to handle student discounts. But as the coffee shop grew and more discount types were added (seniors, loyalty members, different coffee types), the function spiraled out of control. What you're about to see is the reality of technical debt: a function that's been copy-pasted, modified, and patched so many times that it's become unmaintainable.
The Current Code
// Alex's current discount calculation function.
// WARNING: This is the BEFORE code - the mess we need to fix!
fn calculate_price(
coffee_type: &str,
customer_type: &str,
base_price: f64
) -> f64 {
if customer_type == "student" {
if coffee_type == "espresso" {
base_price * 0.8 // 20% off.
} else if coffee_type == "latte" {
base_price * 0.8 // 20% off.
} else if coffee_type == "cappuccino" {
base_price * 0.8 // 20% off.
} else {
base_price * 0.8 // default to 20% off.
}
} else if customer_type == "senior" {
if coffee_type == "espresso" {
base_price * 0.85 // 15% off.
} else if coffee_type == "latte" {
base_price * 0.85 // 15% off.
} else if coffee_type == "cappuccino" {
base_price * 0.85 // 15% off.
} else {
base_price * 0.85 // default to 15% off.
}
} else if customer_type == "loyalty_member" {
if coffee_type == "espresso" {
base_price * 0.9 // 10% off.
} else if coffee_type == "latte" {
base_price * 0.9 // 10% off.
} else if coffee_type == "cappuccino" {
base_price * 0.9 // 10% off.
} else {
base_price * 0.9 // default to 10% off.
}
} else {
base_price // no discount.
}
}
// And this is just for simple percentage discounts!
// There's another function for "buy 2 get 1 free" deals...
// And another for "happy hour" discounts...
// It goes on and on...
"This is insane. I copy-pasted this code 47 times. FORTY-SEVEN! Last week, I changed the student discount from 20% to 25% and had to modify it in 12 different places. I missed three of them and gave some students 20% off while others got 25% off. My accountant is NOT happy."
The Pain Points
Let's break down why this code is a disaster:
Massive Duplication: The same discount logic repeated for every coffee type
Hard to Modify: Changing a discount percentage means hunting through the entire codebase
Error-Prone: Easy to miss one place when making changes
Hard to Test: How do you test this? Mock every possible combination?
Not Scalable: Adding a new discount type means copying everything again

Understanding the Chaos
Let's visualise what's happening in Alex's code:

Notice the problem? Every customer type branches into checking the coffee type, even though the coffee type doesn't affect the discount! This is unnecessary complexity.
The core problems are:
Mixing "What" with "How": The code mixes WHAT discount to apply with HOW to calculate it
Lack of Encapsulation: Discount logic is scattered, not contained
Tight Coupling: The main function knows too much about every discount type
No Abstraction: Each discount is just a number in an if-else chain
Monday morning, 8:37 AM. Alex's phone rings. It's the Marketing Manager, Jamie.
Jamie: "Alex! Great news! We're launching a 'Happy Hour' promotion - 25% off all drinks from 3 PM to 5 PM. Can you have it ready by Friday?"
Alex (already feeling the dread): "Um... sure. Let me check the code..."
Alex opens the laptop. To add Happy Hour discount, Alex needs to:
Add another if-else condition for time checking
Copy-paste it for every coffee type
Make sure it doesn't conflict with student/senior/loyalty discounts
Modify the order processing logic
Update the receipt printing code
Modify the daily sales report generator
Touch at least 8 different files
Alex (voice cracking): "Jamie... I'll need until next Wednesday. Maybe Thursday."
Jamie: "But it's just one discount! The competitor down the street implemented it in a day!"
After the call, Alex stares at the screen. There's got to be a better way.

The Mentor
Tuesday afternoon. Sarah, a regular customer who works as a senior Rust developer at a tech company, notices Alex's frustrated expression.
Sarah: "Rough day?"
Alex: "You could say that. I'm drowning in discount calculations. Every time marketing wants a new promotion, I need to modify code in a dozen places."
Sarah: "Mind if I take a look?"
Alex shows her the code. Sarah's eyes widen.
Sarah: "Ah, I see the problem. You're experiencing the 'God Function' anti-pattern. This function is trying to do everything."
The God Function/Object Anti-Pattern describes a single class or function that takes on far too many responsibilities, knowing and doing too much, violating the Single Responsibility Principle (SRP) by handling unrelated tasks like data access, UI logic, and business rules, leading to code that's hard to maintain, test, understand, and extend due to high coupling and ripple-effect bugs.
Alex: "How would you solve it?"
Sarah: "Ever heard of the Strategy Pattern?"
Alex: "Design patterns? I've heard of them but never really used them..."
Sarah pulls out a napkin and starts drawing.
Sarah: "Think of it this way. You have different discount strategies: student discount, senior discount, loyalty discount. Right now, you're choosing which strategy to use and HOW to calculate it in the same place. That's the problem."
She draws three boxes:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Student │ │ Senior │ │ Loyalty │
│ Discount │ │ Discount │ │ Discount │
│ Strategy │ │ Strategy │ │ Strategy │
│ │ │ │ │ │
│ 20% off │ │ 15% off │ │ 10% off │
└─────────────────┘ └──────────────────┘ └─────────────────┘
Sarah: "Each discount strategy is a separate, independent piece of code. Your order just says, 'Hey, apply whatever discount strategy you have.' It doesn't know or care HOW the discount is calculated."
Alex: "So... separate the WHAT from the HOW?"
WHAT = Which discount to apply (student, senior, loyalty, etc.)
HOW = The actual calculation logic (20% off, 15% off, etc.)
Alex pauses, thinking it through. "So in my current code, I'm doing both at the same time, checking WHO the customer is AND calculating their discount, all tangled together. But with this pattern, I'd just say 'This customer needs the student discount strategy' without knowing or caring that it's 20% off. The strategy itself knows the HOW part, the math. My order just knows WHAT strategy to use." A light bulb moment. "It's like when a customer orders a 'latte', I don't need to know HOW to make every drink. I just hand them the latte they ordered. The barista knows HOW to make it!"
Sarah: "Exactly! In Rust, we can use traits for this. Let me show you."
The Strategy Pattern
The Strategy Pattern is a behavioural design pattern that lets you define a family of algorithms, encapsulate each one, and make them interchangeable.
The Key Idea

The Three Components
Every Strategy Pattern implementation has three essential pieces working together. Let's break them down in a way that makes sense for our coffee shop scenario:

Looking at this diagram, here's what each part means:
Strategy Interface (DiscountStrategy trait): This is the blueprint that defines what all discounts must be able to do. Think of it as a contract: "If you want to be a discount in this system, you MUST be able to calculate a price." It doesn't care HOW you calculate it, just that you CAN.
Concrete Strategies (StudentDiscount, SeniorDiscount, etc.): These are the actual implementations, the workers who follow the blueprint. Each one says, "Yes, I can calculate a discount, and here's MY specific way of doing it." StudentDiscount does 20% off, SeniorDiscount does 15% off, and so on. They all follow the same interface, but each does it their own way.
Context (CoffeeOrder): This is the user of strategies. It's your coffee order that needs a discount applied. The crucial part? It doesn't know OR care which specific discount it has. It just knows, "I have some discount strategy, and when I need to calculate the final price, I'll ask it to do its thing."
Think of payment methods at a store:
Strategy Interface: "Process Payment"
Concrete Strategies: Cash, Credit Card, Mobile Payment, Cryptocurrency
Context: Your purchase
The cashier doesn't need to know HOW each payment method works internally. They just say, "Process this payment," and each method handles it differently.
Sarah: "In your case, the 'Strategy Interface' is 'Calculate Discount.' Each discount type (student, senior, loyalty) is a 'Concrete Strategy.' And your coffee order is the 'Context' that uses whichever strategy is appropriate."
Alex: "So when I add Happy Hour discount, I just create a new strategy? I don't touch any existing code?"
Sarah: "Exactly! That's the Open/Closed Principle: open for extension, closed for modification."
The Open/Closed Principle (OCP) states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
This means you should be able to add new functionality or behaviours to a system without changing its existing, stable source code. The principle significantly improves the modularity and maintainability of code by preventing modifications in one area from introducing bugs in another.
The Refresher

Before we dive into the implementation, let's understand the Rust concepts we'll use.
1. Traits: Rust's Interfaces
A trait defines behaviour that types can implement. It's like a contract - if a type implements a trait, it promises to provide certain functionality.
Learn More: For a comprehensive guide to traits, see the official Rust book chapter on traits.
// A trait is like a promise:
// I can do "these things" FOR SURE.
trait DiscountStrategy {
fn apply_discount(&self, base_price: f64) -> f64;
}
Any type implementing this trait must provide an apply_discount method.
Why traits?
Define shared behaviour: Multiple types can share the same interface without sharing implementation
Polymorphism: Different types can be used interchangeably through the same trait
Zero-cost abstraction: Trait methods can be optimised away at compile time
Composition over inheritance: Rust doesn't have traditional inheritance; traits provide a more flexible alternative
Traits vs Interfaces in Other Languages:
// In Java/C#, you might write:
// interface Discount {
// double applyDiscount(double basePrice);
// }
// In Rust, it's:
trait Discount {
fn apply_discount(&self, base_price: f64) -> f64;
}
The key difference? Rust traits can be implemented for types you don't own, and they support default implementations:
trait DiscountStrategy {
fn apply_discount(&self, base_price: f64) -> f64;
// Default implementation.
fn apply_and_round(&self, base_price: f64) -> f64 {
self.apply_discount(base_price).round()
}
}
Sarah: "Think of traits as capabilities. When you say 'this type implements DiscountStrategy,' you're saying 'this type has the capability to calculate discounts.' The compiler then ensures you actually provide that capability."
2. Box: Dynamic Dispatch
Learn More: Understanding
Box<T>is crucial for heap allocation. Read more in the Rust book's chapter on Box and trait objects.
// This can hold ANY type that implements DiscountStrategy.
let strategy: Box<dyn DiscountStrategy> = Box::new(StudentDiscount);
Breaking it down:
Box<T>: A smart pointer that allocatesTon the heapWhy heap? Because we don't know the size of the concrete type at compile time
Each strategy might have different sizes if they store data
The Box gives us a fixed-size pointer (8 bytes on 64-bit systems)
dyn Trait: Dynamic dispatch - the method to call is determined at runtime"dyn" stands for "dynamic"
Tells Rust: "I don't know the exact type, but it implements this trait"
Uses a vtable (virtual method table) to look up method implementations
Combined: A heap-allocated value of unknown type that implements the trait
Visual representation:

What's a vtable?
A vtable (virtual method table) is a lookup table of function pointers:

When you call strategy.apply_discount(), Rust:
Looks at the vtable pointer
Finds the entry for
apply_discountCalls the function at that address
This small runtime cost (one pointer indirection) gives us flexibility!
Alex: "So every time we call a method, it has to look it up in this table?"
Sarah: "Yes, but it's incredibly fast - just one memory lookup. The trade-off is worth it when you need runtime flexibility."
3. Static vs Dynamic Dispatch
Static Dispatch (Compile-time):
fn calculate<T: DiscountStrategy>(strategy: T, price: f64) -> f64 {
strategy.apply_discount(price)
}
// Compiler generates specific code for each type.
// Fast! But increases binary size.
Dynamic Dispatch (Runtime):
fn calculate(strategy: Box<dyn DiscountStrategy>, price: f64) -> f64 {
strategy.apply_discount(price)
}
// Compiler uses vtable to look up method.
// Slight overhead, but more flexible.
For our use case, we need dynamic dispatch because we don't know which discount strategy to use until runtime (when the customer orders).
4. Ownership and Borrowing in Strategies
trait DiscountStrategy {
// &self means "borrow self immutably".
// The strategy doesn't need to own or modify itself.
fn apply_discount(&self, base_price: f64) -> f64;
}
The &self means our strategies are immutable and can be shared safely.
5. Send and Sync Traits
Learn More: For a deep dive into thread safety, see the Rustonomicon chapter on Send and Sync.
trait DiscountStrategy: Send + Sync {
fn apply_discount(&self, base_price: f64) -> f64;
}
These are marker traits that tell the compiler about thread safety:
Send: A type is Send if it can be transferred between threadsOwnership can move from one thread to another
Most types are Send (integers, strings, your custom structs)
Sync: A type is Sync if it can be shared between threads (via&T)Multiple threads can have immutable references simultaneously
If
&Tis Send, thenTis SyncExample:
Arc<T>(atomic reference counted) is Sync
Imagine you have toy blocks:
Send: You can give your block to another kid (but then you don't have it anymore)
Sync: Multiple kids can look at the same block at the same time (like through a display case)
In our coffee shop:
Send: A discount can be passed from one part of the program to another
Sync: Multiple customers can use the discount rules at the exact same time
The magic: Rust checks this automatically. If you mess up, it won't even let your code compile!
Why do we need this for our coffee shop?
Our web server (Axum) uses async/await and might handle requests on different threads. When a request comes in:
Thread 1: Customer orders coffee → Creates StudentDiscount strategy
Thread 2: Processes the order → Uses that same strategy
Without Send + Sync, the compiler would reject our code because it can't guarantee thread safety!
Visual example of thread safety:
// This is SAFE because DiscountStrategy is Send + Sync.
use std::sync::Arc;
use std::thread;
let strategy = Arc::new(StudentDiscount); // Arc = Atomically Reference Counted.
// Thread 1 can use it.
let strategy_clone = strategy.clone();
let handle1 = thread::spawn(move || {
println!("Thread 1: {}", strategy_clone.apply_discount(10.0));
});
// Thread 2 can also use it simultaneously.
let strategy_clone2 = strategy.clone();
let handle2 = thread::spawn(move || {
println!("Thread 2: {}", strategy_clone2.apply_discount(15.0));
});
handle1.join().unwrap();
handle2.join().unwrap();
Wait, what is Arc?
Let's break down Arc::new(StudentDiscount) because it's crucial for multithreaded applications:
Arc stands for "Atomically Reference Counted" - that's a mouthful, but here's what it means in simple terms:
Reference Counted: It keeps track of HOW MANY parts of your code are using this data. When the count reaches zero, the data is cleaned up automatically.
Atomically: The counting is done in a thread-safe way. Multiple threads can safely update the count at the same time without corrupting it.
Why do we need Arc instead of just Box?
Remember Box<T> from earlier? It's great for single ownership. But here's the problem with threads:
// This WON'T work!
let strategy = Box::new(StudentDiscount);
let handle1 = thread::spawn(move || {
// strategy moved here - Thread 1 owns it now.
});
let handle2 = thread::spawn(move || {
// ERROR! Can't move strategy again - Thread 1 already took it!
});
With Box, once Thread 1 takes ownership (via move), Thread 2 can't use it. But we need BOTH threads to access the same discount!
Arc solves this by allowing shared ownership. This is the beauty of Rust: If your code compiles, you know it's thread-safe. No data races, no mysterious bugs!
Alex: "So Rust won't even let me compile code that could have threading issues?"
Sarah: "Exactly! The compiler is like a really pedantic code reviewer who never gets tired. It might feel strict at first, but it saves you from countless debugging nightmares."
The Solution
Alright! Let's build this thing. We'll start simple and gradually improve.
Want to see the complete working code? Check out the full implementation on GitHub: coffee-shop-api repository
Feel free to clone it and follow along!
Setting Up the Project
# Create a new Rust project.
cargo new coffee-shop-api
cd coffee-shop-api
Open Cargo.toml and add dependencies:
[package]
name = "coffee-shop-api"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors"] }
Why these dependencies?
axum: Web framework (built on top of tokio and hyper)tokio: Async runtime for handling concurrent requestsserde: Serialization/deserialization for JSONtower&tower-http: Middleware utilities
Define the Strategy Trait (The Contract)
Create src/strategies.rs:
// This is the heart of the Strategy Pattern.
// Every discount strategy MUST implement this trait.
pub trait DiscountStrategy: Send + Sync {
/// Calculate the final price after applying the discount.
///
/// # Arguments
/// * `base_price` - The original price before discount.
///
/// # Returns
/// The final price after discount is applied.
fn apply_discount(&self, base_price: f64) -> f64;
/// Get the name of this discount strategy.
/// Useful for logging and displaying to users.
fn name(&self) -> &str;
/// Get a description of how this discount works.
/// Helps customers understand what discount they're getting.
fn description(&self) -> &str;
}
What we just did:
Defined a contract: any discount strategy must implement these three methods
Added
Send + Syncfor thread-safety (required by Axum)
Implement Our First Strategy - Student Discount
Still in src/strategies.rs:
/// Student Discount: 20% off all drinks.
/// Requires valid student ID verification.
pub struct StudentDiscount;
impl DiscountStrategy for StudentDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
// Students get 20% off.
// So they pay 80% of the original price.
base_price * 0.80
}
fn name(&self) -> &str {
"Student Discount"
}
fn description(&self) -> &str {
"20% off all drinks with valid student ID"
}
}
Key points:
pub struct StudentDiscount;- This is a zero-sized type (no data)We implement the trait we defined
The calculation is simple and localized
This strategy doesn't need any configuration or state
Let's test it:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_student_discount_calculation() {
let strategy = StudentDiscount;
let base_price = 10.0;
let final_price = strategy.apply_discount(base_price);
assert_eq!(final_price, 8.0); // 20% off means pay 80%.
}
#[test]
fn test_student_discount_name() {
let strategy = StudentDiscount;
assert_eq!(strategy.name(), "Student Discount");
}
}
Run the test:
cargo test
Output:
running 2 tests
test strategies::tests::test_student_discount_calculation ... ok
test strategies::tests::test_student_discount_name ... ok
Implement More Strategies
Let's add the rest of our discount strategies:
/// Senior Discount: 15% off all drinks.
/// For customers 65 years or older.
pub struct SeniorDiscount;
impl DiscountStrategy for SeniorDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
base_price * 0.85 // 15% off.
}
fn name(&self) -> &str {
"Senior Discount"
}
fn description(&self) -> &str {
"15% off all drinks for seniors (65+)"
}
}
/// Loyalty Card Discount: 10% off all drinks.
/// Plus earns points for future rewards (not implemented here, can be extended).
pub struct LoyaltyDiscount;
impl DiscountStrategy for LoyaltyDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
base_price * 0.90 // 10% off.
}
fn name(&self) -> &str {
"Loyalty Card Discount"
}
fn description(&self) -> &str {
"10% off all drinks + earn points towards free drinks"
}
}
/// No Discount: Full price.
/// For regular customers without any discount eligibility.
pub struct NoDiscount;
impl DiscountStrategy for NoDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
base_price // No discount, pay full price
}
fn name(&self) -> &str {
"Regular Price"
}
fn description(&self) -> &str {
"Standard pricing with no discount"
}
}
Notice the pattern? Each strategy:
Is a simple struct
Implements the
DiscountStrategytraitHas its own calculation logic
Is completely independent of other strategies
Sarah: "See how clean this is? Each discount strategy is self-contained. If the student discount changes, you only modify StudentDiscount. Nothing else."
Creating the Context (CoffeeOrder)
Create src/order.rs:
use crate::models::DiscountType;
use crate::strategies::{
DiscountStrategy, LoyaltyDiscount, NoDiscount, SeniorDiscount, StudentDiscount,
};
/// Represents a coffee order with an applied discount strategy.
///
/// This is the "Context" in Strategy Pattern terminology.
/// It uses a strategy without knowing which specific one it is.
pub struct CoffeeOrder {
pub coffee_type: String,
pub base_price: f64,
// This is the magic! It can be ANY strategy.
discount_strategy: Box<dyn DiscountStrategy>,
}
What's happening here?
CoffeeOrderholds a reference to a discount strategyIt doesn't know or care which specific strategy it is
Box<dyn DiscountStrategy>allows runtime strategy selection
Now let's add methods:
impl CoffeeOrder {
/// Create a new coffee order with the specified discount strategy.
///
/// This is a Factory Method that creates the appropriate strategy
/// based on the customer type.
pub fn new(
coffee_type: String,
base_price: f64,
discount_type: DiscountType
) -> Self {
// FACTORY METHOD: Select the right strategy based on discount type.
let discount_strategy: Box<dyn DiscountStrategy> = match discount_type {
DiscountType::Student => Box::new(StudentDiscount),
DiscountType::Senior => Box::new(SeniorDiscount),
DiscountType::Loyalty => Box::new(LoyaltyDiscount),
DiscountType::None => Box::new(NoDiscount),
};
Self {
coffee_type,
base_price,
discount_strategy,
}
}
/// Calculate the final price by applying the discount strategy.
///
/// THIS IS THE KEY METHOD!
/// It delegates to the strategy without knowing which one it is.
pub fn calculate_final_price(&self) -> f64 {
// Ask the strategy to calculate the discount.
// We don't know if it's student, senior, or loyalty.
// We don't care! That's the power of the pattern.
self.discount_strategy.apply_discount(self.base_price)
}
/// Get the discount amount (convenience method).
pub fn get_discount_amount(&self) -> f64 {
self.base_price - self.calculate_final_price()
}
/// Get the name of the applied discount strategy.
pub fn get_discount_name(&self) -> &str {
self.discount_strategy.name()
}
/// Get the description of the applied discount strategy.
pub fn get_discount_description(&self) -> &str {
self.discount_strategy.description()
}
}
The magic moment: Look at calculate_final_price(). It just calls self.discunt_strategy.apply_discount(). It doesn't have any if-else logic. It doesn't know which discount type it is. That's polymorphism in action!
Define Data Models
Create src/models.rs:
use serde::{Deserialize, Serialize};
/// Request body for creating an order.
#[derive(Debug, Deserialize)]
pub struct OrderRequest {
pub coffee_type: String,
pub customer_type: String, // "student", "senior", "loyalty", or "none".
pub base_price: f64,
}
/// Response body for order calculation.
#[derive(Debug, Serialize)]
pub struct OrderResponse {
pub coffee_type: String,
pub base_price: f64,
pub discount_type: String,
pub discount_description: String,
pub discount_amount: f64,
pub final_price: f64,
}
/// Enum representing different discount types.
#[derive(Debug, Clone, Copy)]
pub enum DiscountType {
Student,
Senior,
Loyalty,
None,
}
impl DiscountType {
/// Parse a string into a DiscountType.
pub fn from_string(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"student" => Some(DiscountType::Student),
"senior" => Some(DiscountType::Senior),
"loyalty" => Some(DiscountType::Loyalty),
"none" => Some(DiscountType::None),
_ => None,
}
}
}
Create API Handlers
Create src/handlers.rs:
use axum::{http::StatusCode, Json};
use crate::models::{DiscountInfo, DiscountsResponse, OrderRequest, OrderResponse};
use crate::order::CoffeeOrder;
use crate::models::DiscountType;
/// Handler for POST /order.
///
/// This endpoint:
/// 1. Receives order details from the client
/// 2. Validates the input
/// 3. Creates a CoffeeOrder with appropriate strategy
/// 4. Calculates the final price
/// 5. Returns the result
pub async fn calculate_order(
Json(payload): Json<OrderRequest>,
) -> Result<Json<OrderResponse>, StatusCode> {
// Validation: Check if base price is valid.
if payload.base_price <= 0.0 {
return Err(StatusCode::BAD_REQUEST);
}
// Parse the customer type string into a DiscountType enum.
let discount_type = DiscountType::from_string(&payload.customer_type)
.ok_or(StatusCode::BAD_REQUEST)?;
// Create the order with the appropriate discount strategy.
// This is where the Factory Method pattern comes in.
let order = CoffeeOrder::new(
payload.coffee_type.clone(),
payload.base_price,
discount_type,
);
// Calculate prices using the strategy.
let final_price = order.calculate_final_price();
let discount_amount = order.get_discount_amount();
// Build response.
let response = OrderResponse {
coffee_type: payload.coffee_type,
base_price: payload.base_price,
discount_type: order.get_discount_name().to_string(),
discount_description: order.get_discount_description().to_string(),
discount_amount,
final_price,
};
Ok(Json(response))
}
Flow visualisation:

Set Up the Web Server
Update src/main.rs:
use axum::{
routing::{get, post},
Router,
};
use tower_http::cors::CorsLayer;
mod handlers;
mod models;
mod order;
mod strategies;
#[tokio::main]
async fn main() {
// Build our application with routes.
let app = Router::new()
.route("/order", post(handlers::calculate_order))
.route("/discounts", get(handlers::list_discounts))
.layer(CorsLayer::permissive());
// Run the server.
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("Rusty Bean Coffee Shop API running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
The Implementation

Let's see the complete files:
Complete src/strategies.rs
// The Strategy Pattern: Each discount strategy implements this trait.
// This allows us to swap discount algorithms at runtime without changing the order logic.
/// The core trait that all discount strategies must implement.
/// This is the "Strategy" in Strategy Pattern.
pub trait DiscountStrategy: Send + Sync {
/// Calculate the final price after applying the discount.
fn apply_discount(&self, base_price: f64) -> f64;
/// Get the name of this discount strategy.
fn name(&self) -> &str;
/// Get a description of how this discount works.
fn description(&self) -> &str;
}
// ============================================================================
// CONCRETE STRATEGIES - Each one is a different discount algorithm
// ============================================================================
/// Student Discount: 20% off all drinks.
/// Used for customers with valid student ID.
pub struct StudentDiscount;
impl DiscountStrategy for StudentDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
base_price * 0.80 // 20% off.
}
fn name(&self) -> &str {
"Student Discount"
}
fn description(&self) -> &str {
"20% off all drinks with valid student ID"
}
}
/// Senior Discount: 15% off all drinks.
/// Used for customers 65 years or older.
pub struct SeniorDiscount;
impl DiscountStrategy for SeniorDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
base_price * 0.85 // 15% off.
}
fn name(&self) -> &str {
"Senior Discount"
}
fn description(&self) -> &str {
"15% off all drinks for seniors (65+)"
}
}
/// Loyalty Card Discount: 10% off all drinks.
/// Also earns points for future rewards (can be extended for this feature).
pub struct LoyaltyDiscount;
impl DiscountStrategy for LoyaltyDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
base_price * 0.90 // 10% off.
}
fn name(&self) -> &str {
"Loyalty Card Discount"
}
fn description(&self) -> &str {
"10% off all drinks + earn points towards free drinks"
}
}
/// No Discount: Regular price.
/// Used for customers without any discount eligibility.
pub struct NoDiscount;
impl DiscountStrategy for NoDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
base_price // No discount applied.
}
fn name(&self) -> &str {
"Regular Price"
}
fn description(&self) -> &str {
"Standard pricing with no discount"
}
}
// ============================================================================
// BONUS: Happy Hour Discount (Added easily thanks to Strategy Pattern!)
// ============================================================================
/// Happy Hour Discount: 25% off all drinks.
/// Only available during happy hour (3-5 PM).
/// This shows how easy it is to add new strategies!
#[allow(dead_code)] // We're not using this in the API yet, but it's here to demonstrate extensibility.
pub struct HappyHourDiscount;
impl DiscountStrategy for HappyHourDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
base_price * 0.75 // 25% off.
}
fn name(&self) -> &str {
"Happy Hour Discount"
}
fn description(&self) -> &str {
"25% off all drinks during happy hour (3-5 PM)"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_student_discount() {
let strategy = StudentDiscount;
assert_eq!(strategy.apply_discount(10.0), 8.0);
}
#[test]
fn test_senior_discount() {
let strategy = SeniorDiscount;
assert_eq!(strategy.apply_discount(10.0), 8.5);
}
#[test]
fn test_loyalty_discount() {
let strategy = LoyaltyDiscount;
assert_eq!(strategy.apply_discount(10.0), 9.0);
}
#[test]
fn test_no_discount() {
let strategy = NoDiscount;
assert_eq!(strategy.apply_discount(10.0), 10.0);
}
}
Testing Our Solution
Running the Server:
cargo run
Output:
Rusty Bean Coffee Shop API running on http://localhost:3000
Compiling coffee-shop-api v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 3.21s
Running `target/debug/coffee-shop-api`
Success! The server is running. Now let's test it.
Testing with curl
Let's test our API with some real requests:
Test 1: Student orders a latte
curl -X POST http://localhost:3000/order \
-H "Content-Type: application/json" \
-d '{
"coffee_type": "Latte",
"customer_type": "student",
"base_price": 5.00
}'
Response:
{
"coffee_type": "Latte",
"base_price": 5.0,
"discount_type": "Student Discount",
"discount_description": "20% off all drinks with valid student ID",
"discount_amount": 1.0,
"final_price": 4.0
}
The student gets 20% off their $5.00 latte, paying only $4.00.
Test 2: Senior orders a cappuccino
curl -X POST http://localhost:3000/order \
-H "Content-Type: application/json" \
-d '{
"coffee_type": "Cappuccino",
"customer_type": "senior",
"base_price": 4.50
}'
Response:
{
"coffee_type": "Cappuccino",
"base_price": 4.5,
"discount_type": "Senior Discount",
"discount_description": "15% off all drinks for seniors (65+)",
"discount_amount": 0.675,
"final_price": 3.825
}
The senior citizen gets 15% off.
Test 3: Regular customer orders an espresso
curl -X POST http://localhost:3000/order \
-H "Content-Type: application/json" \
-d '{
"coffee_type": "Espresso",
"customer_type": "none",
"base_price": 3.00
}'
Response:
{
"coffee_type": "Espresso",
"base_price": 3.0,
"discount_type": "Regular Price",
"discount_description": "Standard pricing with no discount",
"discount_amount": 0.0,
"final_price": 3.0
}
Regular customers pay full price.
Missing Pieces
Let's add the missing handler for listing available discounts:
Complete src/handlers.rs:
use crate::models::DiscountType;
use crate::models::{DiscountInfo, DiscountsResponse, OrderRequest, OrderResponse};
use crate::order::CoffeeOrder;
use crate::strategies::{
DiscountStrategy, LoyaltyDiscount, NoDiscount, SeniorDiscount, StudentDiscount,
};
use axum::{Json, http::StatusCode};
/// Calculates the final price with the selected discount strategy.
/// REQUEST METHOD: POST
/// REQUEST URL: /order
/// REQUEST BODY: { "coffee_type": "latte", "customer_type": "student", "base_price": 5.0 }
pub async fn calculate_order(
Json(payload): Json<OrderRequest>,
) -> Result<Json<OrderResponse>, StatusCode> {
if payload.base_price <= 0.0 {
return Err(StatusCode::BAD_REQUEST);
}
let discount_type =
DiscountType::from_string(&payload.customer_type).ok_or(StatusCode::BAD_REQUEST)?;
let order = CoffeeOrder::new(
payload.coffee_type.clone(),
payload.base_price,
discount_type,
);
let final_price = order.calculate_final_price();
let discount_amount = order.get_discount_amount();
let response = OrderResponse {
coffee_type: payload.coffee_type,
base_price: payload.base_price,
discount_type: order.get_discount_name().to_string(),
discount_description: order.get_discount_description().to_string(),
discount_amount,
final_price,
};
Ok(Json(response))
}
/// Lists all available discount strategies.
/// REQUEST METHOD: GET
/// REQUEST URL: /discounts
pub async fn list_discounts() -> Json<DiscountsResponse> {
let student: Box<dyn DiscountStrategy> = Box::new(StudentDiscount);
let senior: Box<dyn DiscountStrategy> = Box::new(SeniorDiscount);
let loyalty: Box<dyn DiscountStrategy> = Box::new(LoyaltyDiscount);
let none: Box<dyn DiscountStrategy> = Box::new(NoDiscount);
let discounts = vec![
DiscountInfo {
code: "student".to_string(),
name: student.name().to_string(),
description: student.description().to_string(),
},
DiscountInfo {
code: "senior".to_string(),
name: senior.name().to_string(),
description: senior.description().to_string(),
},
DiscountInfo {
code: "loyalty".to_string(),
name: loyalty.name().to_string(),
description: loyalty.description().to_string(),
},
DiscountInfo {
code: "none".to_string(),
name: none.name().to_string(),
description: none.description().to_string(),
},
];
Json(DiscountsResponse {
available_discounts: discounts,
})
}
Complete src/models.rs:
use serde::{Deserialize, Serialize};
/// Request body for creating an order.
#[derive(Debug, Deserialize)]
pub struct OrderRequest {
pub coffee_type: String,
pub customer_type: String, // "student", "senior", "loyalty", or "none".
pub base_price: f64,
}
/// Response body for order calculation.
#[derive(Debug, Serialize)]
pub struct OrderResponse {
pub coffee_type: String,
pub base_price: f64,
pub discount_type: String,
pub discount_description: String,
pub discount_amount: f64,
pub final_price: f64,
}
/// Response for listing available discounts.
#[derive(Debug, Serialize)]
pub struct DiscountInfo {
pub code: String,
pub name: String,
pub description: String,
}
/// Response body for listing all available discounts.
#[derive(Debug, Serialize)]
pub struct DiscountsResponse {
pub available_discounts: Vec<DiscountInfo>,
}
/// Enum representing different discount types.
/// This is used to select which strategy to use.
#[derive(Debug, Clone, Copy)]
pub enum DiscountType {
Student,
Senior,
Loyalty,
None,
}
impl DiscountType {
/// Parse a string into a DiscountType.
/// Returns None if the string doesn't match any known type.
pub fn from_string(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"student" => Some(DiscountType::Student),
"senior" => Some(DiscountType::Senior),
"loyalty" => Some(DiscountType::Loyalty),
"none" => Some(DiscountType::None),
_ => None,
}
}
/// Convert DiscountType to a string code.
#[allow(dead_code)]
pub fn to_string(&self) -> &str {
match self {
DiscountType::Student => "student",
DiscountType::Senior => "senior",
DiscountType::Loyalty => "loyalty",
DiscountType::None => "none",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_discount_type_parsing() {
assert!(matches!(
DiscountType::from_string("student"),
Some(DiscountType::Student)
));
assert!(matches!(
DiscountType::from_string("SENIOR"),
Some(DiscountType::Senior)
));
assert!(DiscountType::from_string("invalid").is_none());
}
}
Test the discounts endpoint:
curl http://localhost:3000/discounts
Response:
{
"discounts": [
{
"name": "Student Discount",
"description": "20% off all drinks with valid student ID",
"percentage": "20%",
"example_savings": "$2.00 off on a $10.00 drink"
},
{
"name": "Senior Discount",
"description": "15% off all drinks for seniors (65+)",
"percentage": "15%",
"example_savings": "$1.50 off on a $10.00 drink"
},
{
"name": "Loyalty Card Discount",
"description": "10% off all drinks + earn points towards free drinks",
"percentage": "10%",
"example_savings": "$1.00 off on a $10.00 drink"
},
{
"name": "Regular Price",
"description": "Standard pricing with no discount",
"percentage": "0%",
"example_savings": "$0.00 off on a $10.00 drink"
}
]
}
Let's run our unit tests to make sure everything works:
cargo test
Output:
running 7 tests
test strategies::tests::test_student_discount ... ok
test strategies::tests::test_senior_discount ... ok
test strategies::tests::test_loyalty_discount ... ok
test strategies::tests::test_no_discount ... ok
test order::tests::test_order_with_student_discount ... ok
test order::tests::test_order_with_no_discount ... ok
test order::tests::test_order_with_senior_discount ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Our implementation is solid.
Alex leans back in the chair, a smile spreading across their face.

The Resolution
Three weeks later. Friday evening, 7:15 PM.
Alex stands behind the counter, watching the last customers of the day leave. The laptop is open, showing a dashboard of the day's sales. Everything has been running smoothly since implementing the Strategy Pattern.
Sarah walks in, ordering her usual cappuccino.
Sarah: "So? How's the new system working out?"
Alex (beaming): "Sarah, you saved my business. Remember that Happy Hour discount that Jamie wanted? The one that would've taken me a week to implement with the old code?"
Sarah: "The one that was going to ruin your life?"
Alex: "I implemented it in 45 minutes."
Sarah: (raising her eyebrows) "Seriously?"
Alex: "Watch this."
Alex opens the laptop and added a new strategy in src/strategies.rs:
/// Happy Hour Discount: 25% off all drinks.
/// Valid from 3 PM to 5 PM on weekdays.
pub struct HappyHourDiscount;
impl DiscountStrategy for HappyHourDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
base_price * 0.75 // 25% off.
}
fn name(&self) -> &str {
"Happy Hour Special"
}
fn description(&self) -> &str {
"25% off all drinks (3 PM - 5 PM weekdays)"
}
}
Then adds one line to models.rs:
pub enum DiscountType {
Student,
Senior,
Loyalty,
HappyHour, // <- Just this!
None,
}
And updates the parser:
impl DiscountType {
pub fn from_string(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"student" => Some(DiscountType::Student),
"senior" => Some(DiscountType::Senior),
"loyalty" => Some(DiscountType::Loyalty),
"happyhour" => Some(DiscountType::HappyHour), // <- And this!
"none" => Some(DiscountType::None),
_ => None,
}
}
}
Finally, updates the factory in order.rs:
let discount_strategy: Box<dyn DiscountStrategy> = match discount_type {
DiscountType::Student => Box::new(StudentDiscount),
DiscountType::Senior => Box::new(SeniorDiscount),
DiscountType::Loyalty => Box::new(LoyaltyDiscount),
DiscountType::HappyHour => Box::new(HappyHourDiscount), // <- Just this line!
DiscountType::None => Box::new(NoDiscount),
};
Alex: "That's it. Three small changes. No touching the existing strategies. No modifying the order calculation logic. No hunting through 12 different files. Just add the new strategy and plug it in."
Sarah: "And it just... works?"
Alex: "It just works. And you know what the best part is?"
Sarah: "What?"
Alex: "Last month, Jamie asked for a 'Buy One Get One Free' discount for couples. With the old code, I would've panicked. But now?"
Alex shows Sarah another strategy:
/// BOGO Discount for couples: Buy one drink, get second 50% off.
pub struct CoupleDiscount;
impl DiscountStrategy for CoupleDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
// First drink: full price.
// Second drink: 50% off.
// Average per drink: 75% of normal price.
base_price * 0.75
}
fn name(&self) -> &str {
"Couples Special"
}
fn description(&self) -> &str {
"Buy one drink, get second 50% off"
}
}
Alex: "Fifteen minutes. Implemented, tested, and deployed."
Sarah (genuinely impressed): "You've really internalised this pattern."
Alex: "You know what's crazy? I was up until 2 AM for three weeks straight, fighting with that if-else nightmare. Now I close on time, the code is clean, and I actually enjoy adding new features. I even started a blog about it!"
Sarah: "A blog?"
Alex: "Yeah, 'The Coffee Shop Crisis' Figured other people might be struggling with the same stuff. Got 500 readers in the first week."
Sarah smiles, picking up her cappuccino: "That's the power of good software design, Alex. It doesn't just make your code better, it makes your life better."
Alex: "Thank you, Sarah. Really. You didn't just help me fix my code. You taught me how to think about problems differently."
As Sarah heads out, Alex looks at the clean, organised codebase on the screen. Three months ago, this coffee shop was a dream filled with stress. Now it's a dream that works.
And it's all because Alex learned to think in patterns.
The Use (And No Use)
The Strategy Pattern is powerful, but it's not always the right tool. Here's when to use it and when to avoid it.
Use Strategy Pattern When:
1. You Have Family of Algorithms
When you have multiple ways to accomplish the same task:
// Good use case: Different payment methods.
trait PaymentStrategy {
fn process_payment(&self, amount: f64) -> Result<(), String>;
}
struct CreditCardPayment;
struct PayPalPayment;
struct CryptoPayment;
2. Behaviour Needs to Change at Runtime
When you don't know which algorithm to use until runtime:
// The discount applied depends on customer data we fetch at runtime.
let customer = fetch_customer_from_database(customer_id);
let discount = match customer.status {
CustomerStatus::New => Box::new(WelcomeDiscount),
CustomerStatus::VIP => Box::new(VIPDiscount),
CustomerStatus::Regular => Box::new(NoDiscount),
};
3. You Want to Avoid Complex Conditionals
When you have this:
// DON'T DO THIS
fn calculate_shipping(method: &str, weight: f64) -> f64 {
if method == "standard" {
if weight < 5.0 {
5.99
} else if weight < 20.0 {
12.99
} else {
25.99
}
} else if method == "express" {
if weight < 5.0 {
15.99
} else if weight < 20.0 {
28.99
} else {
50.99
}
} else if method == "overnight" {
// ... you get the idea.
}
}
Do this instead:
// DO THIS.
trait ShippingStrategy {
fn calculate_cost(&self, weight: f64) -> f64;
}
struct StandardShipping;
impl ShippingStrategy for StandardShipping {
fn calculate_cost(&self, weight: f64) -> f64 {
if weight < 5.0 { 5.99 }
else if weight < 20.0 { 12.99 }
else { 25.99 }
}
}
4. Open/Closed Principle Matters
When you want to add new behaviours without modifying existing code:
// Adding a new discount doesn't require touching existing code.
pub struct SeasonalDiscount {
percentage: f64,
}
impl DiscountStrategy for SeasonalDiscount {
fn apply_discount(&self, base_price: f64) -> f64 {
base_price * (1.0 - self.percentage)
}
fn name(&self) -> &str {
"Seasonal Sale"
}
fn description(&self) -> &str {
"Limited time seasonal discount"
}
}
Don't Use Strategy Pattern When:
1. You Only Have One Algorithm
If there's only one way to do something, don't over-engineer:
// BAD: Unnecessary abstraction.
trait EmailSender {
fn send_email(&self, to: &str, body: &str);
}
struct SmtpEmailSender; // Only one implementation.
// GOOD: Just write the function.
fn send_email(to: &str, body: &str) {
// Send email using SMTP.
}
2. The Algorithms are Simple and Rarely Change
// BAD: Over-engineering a simple calculation.
trait TaxStrategy {
fn calculate_tax(&self, amount: f64) -> f64;
}
// GOOD: Simple function is fine.
fn calculate_tax(amount: f64, tax_rate: f64) -> f64 {
amount * tax_rate
}
3. Performance is Critical and Predictable
If you need maximum performance and know the algorithm at compile time:
// Use generics (static dispatch) instead of trait objects (dynamic dispatch).
fn process<T: Strategy>(strategy: T, data: &Data) {
strategy.execute(data) // No vtable lookup, can be inlined.
}
4. The Strategies Share a Lot of Common Code
If implementations are mostly the same with small variations:
// Instead of Strategy Pattern, use configuration
struct DiscountConfig {
percentage: f64,
min_purchase: f64,
}
fn apply_discount(price: f64, config: &DiscountConfig) -> f64 {
if price >= config.min_purchase {
price * (1.0 - config.percentage)
} else {
price
}
}
The Conclusion
We started with Alex's nightmare: a tangled mess of if-else statements that made every change painful. Through the Strategy Pattern, we transformed that chaos into clean, maintainable code.
What we learned:
Strategy Pattern separates WHAT from HOW: The order doesn't know how discounts are calculated, it just knows to apply them
Traits enable polymorphism in Rust: Different types sharing the same interface
Box enables runtime flexibility: Choose algorithms dynamically
Open/Closed Principle in action: Add new discounts without modifying existing code
Thread safety with Send + Sync: Our strategies work seamlessly in async contexts
But more importantly, we learned that good software design isn't just about the code, it's about making your life easier. Alex went from drowning in technical debt to enjoying feature development. That's the real power of design patterns.
Written with ❤️ by a developer who's been in Alex's shoes. We've all had our 2 AM debugging sessions. Design patterns are the coffee that keeps our code awake and healthy.
Special thanks to the Rust community for building such an amazing language and ecosystem.
Stay caffeinated, stay rusty! 🦀



