Algebraic Data Types as Architecture: Rustifying the Business Domain
In Rust, your enums aren’t just data. They’re your domain. Your compiler becomes your co-architect.
Algebraic Data Types as Architecture: Rustifying the Business Domain
In Rust, your enums aren’t just data. They’re your domain. Your compiler becomes your co-architect.

The Day I Deleted 300 Lines of If-Else Logic
We had just finished building the order processing system in our e-commerce backend. It worked. Kinda. But the code was… fragile.
- If statements nested three levels deep
- Business rules duplicated across modules
- One wrong string variant and the system silently broke
That’s when I discovered the true architectural power of Rust’s Algebraic Data Types — or as we lovingly call them, enums.
This isn’t just about modeling states. This is about modeling truth. Domain truth.
So let’s dive into how enums + structs + the type system let us design business logic that’s:
- Easier to extend
- Impossible to misuse
- Testable by construction
Let’s talk about Rustifying the business domain.
Architecture Overview
We’ll use a simplified Order Processing domain.
Business Rules:
- An order can be:
Pending,Paid,Shipped,Cancelled - Only
Paidorders can be shipped - Cancelled orders can’t be paid or shipped
- Shipped orders are immutable
Instead of modeling this with strings or flags, let’s reflect reality in types.
Code: Naive Version (How We Used to Do It)
struct Order {
id: Uuid,
status: String, // "pending", "paid", "shipped", "cancelled"
}
fn ship(order: &mut Order) {
if order.status == "paid" {
order.status = "shipped".to_string();
} else {
panic!("Cannot ship unless paid");
}
}
This is legal Rust, but it’s illegal business logic.
✅ Rustified Version with ADTs
Step 1: Encode Legal States
#[derive(Debug)]
pub enum OrderState {
Pending,
Paid,
Shipped,
Cancelled,
}
Step 2: Embed in Struct
pub struct Order {
pub id: Uuid,
pub state: OrderState,
}
Now instead of “status” being a typo-prone string, it’s a closed set of valid states.
Step 3: Use the Type System for Business Rules
impl Order {
pub fn pay(self) -> Result<Order, String> {
match self.state {
OrderState::Pending => Ok(Order { state: OrderState::Paid, ..self }),
_ => Err("Can only pay pending orders".into()),
}
}
pub fn ship(self) -> Result<Order, String> {
match self.state {
OrderState::Paid => Ok(Order { state: OrderState::Shipped, ..self }),
_ => Err("Can only ship paid orders".into()),
}
}
pub fn cancel(self) -> Result<Order, String> {
match self.state {
OrderState::Shipped => Err("Can't cancel shipped order".into()),
_ => Ok(Order { state: OrderState::Cancelled, ..self }),
}
}
}
✅ Compile-time state validation
✅ Impossible to create invalid transitions without explicitly opting in
✅ No more "shipped".to_string() typos
Code Flow: Order Lifecycle
User Checkout
│
▼
Order { state: Pending }
│
└─── pay() ──▶ Order { state: Paid }
│
└── ship() ──▶ Order { state: Shipped }
Invalid transitions are just… impossible unless you handle them with Result.
Bonus: Type-State Pattern (For Extra Compile-Time Guarantees)
What if we wanted hard compile-time separation of states?
struct PendingOrder { id: Uuid }
struct PaidOrder { id: Uuid }
struct ShippedOrder { id: Uuid }
impl PendingOrder {
fn pay(self) -> PaidOrder {
PaidOrder { id: self.id }
}
}
impl PaidOrder {
fn ship(self) -> ShippedOrder {
ShippedOrder { id: self.id }
}
}
This guarantees:
- You can’t ship a pending order (no
.ship()method onPendingOrder) - You can’t cancel a shipped order, unless you build the path explicitly
It’s like business logic encoded in the type system.
Real Example: Payment Processing
#[derive(Debug)]
pub enum PaymentStatus {
Authorized,
Captured,
Refunded,
Failed,
}
pub struct Payment {
pub id: Uuid,
pub amount: u32,
pub status: PaymentStatus,
}
impl Payment {
pub fn capture(&mut self) -> Result<(), PaymentError> {
match self.status {
PaymentStatus::Authorized => {
self.status = PaymentStatus::Captured;
Ok(())
}
_ => Err(PaymentError::InvalidState),
}
}
}
Benchmarks: String Status vs Enum Match
| Approach | Avg Time per Check | Allocation |
| --------------------- | ------------------ | ---------- |
| `if status == "paid"` | 55 ns | 1 alloc |
| `match enum` | 13 ns | 0 alloc |
✅ Enums are faster ✅ Enums are safer ✅ Enums are meaningful
Pattern: Exhaustive Matching for Future Safety
match payment.status {
PaymentStatus::Authorized => { /* ok */ }
PaymentStatus::Captured => { /* skip */ }
// Compiler warns if we miss a variant!
}
Add a new variant like Chargeback, and the compiler tells you every place that forgot to handle it.
That’s architecture-level visibility, enforced at compile time.
Emotions You Will Feel
| Phase | Emotion | Why? |
| -------------------------- | --------- | -------------------------------------------- |
| Rewriting `String` to enum | Annoyed | “Ugh… so much boilerplate…” |
| Removing bugs | Surprised | “Wait, how did we never catch this before?” |
| Modeling type-states | Empowered | “I *can’t* misuse this object anymore.” |
| Code review | Peaceful | “Oh, it’s impossible to do the wrong thing.” |
Bonus: Using Enums in APIs (with serde)
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OrderState {
Pending,
Paid,
Shipped,
Cancelled,
}
Output in JSON:
{
"state": "paid"
}
Perfect for:
- REST APIs
- Kafka messages
- Human-readable logs
Code Structure (Architecture via ADTs)
domain/
└── mod.rs
└── order.rs ← all states & transitions modeled
└── payment.rs
infra/
└── db.rs
└── kafka.rs
api/
└── handlers.rs ← uses `Result<Order, DomainError>`
This is hexagonal architecture, Rustified.
Key Takeaways
- Rust’s enums aren’t “just” enums — they’re Algebraic Data Types
- ADTs let you model your domain logic directly, not as strings or flags
- The compiler becomes your business logic gatekeeper
- Errors surface early, logic becomes testable by default
- Future changes (e.g., new states) are compiler-visible
- Enums + match = speed, clarity, correctness
Final Thoughts
In most languages, architecture is something you hope survives contact with reality.
In Rust, it’s something the compiler guards for you.
By modeling your domain truthfully — with enums, structs, and the type system — you don’t just write correct code. You write code that reflects how your business works.
No more “pending” being typed wrong. No more invalid transitions. No more giant if-else trees.
Just types. And trust.
Rustify your domain. Let your types do the thinking.
메타데이터
- post_id
- 5f956fbdbe4e
- slug
- algebraic-data-types-as-architecture-rustifying-the-business-domain-5f956fbdbe4e
- url
- https://medium.com/@bugsybits/algebraic-data-types-as-architecture-rustifying-the-business-domain-5f956fbdbe4e
- canonical_url
- https://medium.com/@bugsybits/algebraic-data-types-as-architecture-rustifying-the-business-domain-5f956fbdbe4e
- author_url
- https://medium.com/@bugsybits
- status
- ok
- fetched_at
- 2026-07-23 16:21:46