Clean Architecture in Practice: Lessons from Real-World Projects

Clean Architecture in Practice: Lessons from Real-World Projects

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.