What Clean Architecture Looks Like in Real Code
Last article I explained Clean Architecture theory. Now let me show you what it actually looks like in a real project.
This is from a production e-commerce system I've been working on for 3 years. Real code. Real problems. Real solutions.
The Feature: Processing a Refund
Business rule: Users can request refunds within 30 days. We charge a 10% restocking fee unless the product is defective.
Layer 1: The Entity (Business Rules)
class Refund:
def __init__(self, order, reason, amount):
self.order = order
self.reason = reason
self.amount = amount
def calculate_refund_amount(self):
# Business rule: 10% restocking fee
if self.reason == "defective":
return self.amount # Full refund
else:
return self.amount * 0.9 # 10% fee
def is_eligible(self):
# Business rule: 30 day limit
days_since_purchase = (datetime.now() - self.order.date).days
return days_since_purchase <= 30
Pure business logic. No database. No HTTP. No framework. Just rules.
Layer 2: The Use Case (Application Logic)
class ProcessRefundUseCase:
def __init__(self, order_repo, payment_gateway, notification_service):
self.order_repo = order_repo
self.payment_gateway = payment_gateway
self.notification_service = notification_service
def execute(self, order_id, reason):
# Load order
order = self.order_repo.find_by_id(order_id)
if not order:
raise OrderNotFound()
# Create refund
refund = Refund(order, reason, order.total)
# Check eligibility
if not refund.is_eligible():
raise RefundNotEligible("Refund period expired")
# Process refund
amount = refund.calculate_refund_amount()
self.payment_gateway.refund(order.payment_id, amount)
# Update order status
order.status = "refunded"
self.order_repo.save(order)
# Notify customer
self.notification_service.send_refund_confirmation(order.user, amount)
return refund
Notice: No HTTP code. No database queries. Just orchestration of business logic.
Layer 3: The Controller (HTTP Adapter)
@app.post('/orders/{order_id}/refund')
def refund_order(order_id: str, request: RefundRequest):
try:
refund = process_refund_use_case.execute(
order_id=order_id,
reason=request.reason
)
return {"status": "success", "refund_amount": refund.amount}
except OrderNotFound:
return {"error": "Order not found"}, 404
except RefundNotEligible as e:
return {"error": str(e)}, 400
Thin layer. Just translates HTTP → use case → HTTP response.
Layer 4: Infrastructure (Database, External APIs)
class PostgresOrderRepository:
def find_by_id(self, order_id):
row = self.db.query("SELECT * FROM orders WHERE id = %s", order_id)
return Order(id=row['id'], total=row['total'], ...)
def save(self, order):
self.db.execute("UPDATE orders SET status = %s WHERE id = %s",
order.status, order.id)
class StripePaymentGateway:
def refund(self, payment_id, amount):
stripe.Refund.create(payment_intent=payment_id, amount=amount)
All the dirty details. Tomorrow we switch to PayPal? Just write a new PayPalPaymentGateway. Use case doesn't change.
What This Gets You
- Easy Testing: Test the use case without touching database or Stripe
- Easy Changes: Switch from Postgres to MongoDB? Just change the repository
- Easy Understanding: New developer reads the use case, understands the feature in 2 minutes
Lessons from 3 Years
1. Don't Overdo It
For simple CRUD (create/read/update/delete), just write a controller + database query. Clean Architecture is overkill.
Save it for complex business logic.
2. Start with Use Cases
When adding a feature, write the use case first. Then build infrastructure around it. Not the other way around.
3. Keep Entities Small
An entity should represent one business concept. Order, Refund, User. Not OrderManager with 20 methods.
Try It Monday
Pick one feature. Extract the business logic into an entity/use case. See how it feels.
You'll know within a week if this helps your codebase or just adds ceremony.
For me? It's the difference between code I understand in 6 months vs. code I've forgotten entirely.