When Clean Code Becomes Dirty
I need to be honest about something. A few years back, I created what I thought was my masterpiece. It was beautiful, at least in my head, every design pattern you could imagine, perfectly layered architecture, abstractions on abstractions. The kind of code that would make architecture astronauts weep with joy. I never ever missed a single article about clean architecture. I read many books about design patterns. I spent countless hours trying to understand how to properly abstract my code or which design pattern to use in a very simple case. I am looking back now and I am embarrassed how did I fall into this trap! Want to see something embarrassing? Here’s what I actually wrote:
class AbstractFactoryProviderServiceImpl(IAbstractFactoryProviderService):
# Even I don't remember what this does anymore
pass
I saw that I wasnt alone, this code everyone try to write after reading too many Medium articles about clean architecture. Where every class name is longer than a Starbucks order, and every feature requires a PhD in Enterprise Pattern Studies to implement.
I believe We have all been there. That moment when you realize your simple button click handler somehow grew into a doctoral thesis in design patterns. When your stack trace is longer than your actual code. When a junior developer asks you why it takes seventeen interfaces to validate an email address, and you honestly cant remember anymore.
That said, there iss the other side, the “just make it work” crowd. Where every function is 300 lines long, variables are named ‘x’ and ‘temp’, and the only comments are “Don’t touch this” and “I don’t know why this works.” Which honestly, these crowd are nightmare for every developer who touches their code.
Let us see how this chronic disease of clean architecture and abstractions spread through the tech industry.
The Promise vs. The Reality
Remember that beautiful moment in the architecture meeting? “With these abstractions, we’ll be able to swap out any component easily!” a bold claim from the architect, drawing even more boxes on the whiteboard. “It’ll be maintainable! Flexible! Future-proof! and enterprise-ready!” The reality? Let’s see how that played out:
# The Promise:
def clean_architecture_dream():
return swap_components_easily()
# The Reality:
def actual_nightmare():
try:
untangle_dependency_hell()
figure_out_which_abstraction_actually_does_the_thing()
pray_for_the_code_to_work()
except EverythingIsOnFire:
return to_monolith()
We can easly see what this looks like in real life. Take a simple “Hello World”:
# Before: Maintainable, Flexible, Future-proof, Enterprise Edition™
def enterprise_hello_world():
return AbstractGreetingStrategyFactory()
.create_greeting_strategy()
.execute_greeting_protocol()
# After: Actually Useful Edition
def hello_world():
return "Hello, World!"
# Look ma, no factories!
Or perhaps a button click handler:
# What the Medium article suggests
class EnterpriseReadyButtonClickHandler(AbstractClickHandlerBase):
def __init__(self,
click_strategy_factory: IClickStrategyFactory,
event_propagation_manager: IEventPropagationManager,
state_tracking_provider: IStateTrackingProvider):
# 50 lines of dependency injection later...
pass
# What actually works in production
def handle_button_click(button_id):
if not button_id:
log_error("Missing button ID")
return
update_ui(button_id)
notify_backend(button_id)
# Done. Ships to production. Makes money.
A Tale of Two Approaches
Let’s look at what happens when two developers tackle the same problem: building a checkout system. This isnt hypothetical this is real production code with real consequences.
Tom, fresh from reading every Medium article about clean architecture, started with:
class EnterpriseCheckoutStrategyFactoryProvider:
def __init__(self,
validation_factory: IValidationFactory,
payment_strategy: IPaymentStrategyFactory,
cart_processor: ICartProcessingChain):
# 3 days later, still setting up the architecture...
pass
Meanwhile, Bob quietly committed this:
class Checkout:
def process(self, cart, payment):
# Validate cart items first
if not cart.has_items():
return Error("Empty cart")
if not cart.all_items_in_stock():
return Error("Some items out of stock")
# Calculate final price including discounts
total = cart.calculate_total()
if cart.has_discount():
total = apply_discount(total, cart.discount)
# Process payment
try:
payment_result = payment.charge(total)
if payment_result.success:
clear_cart(cart.id)
send_confirmation(cart.user_email)
return Success(order_id=generate_order_id())
return Error(f"Payment failed: {payment_result.error}")
except PaymentError as e:
log.error(f"Payment processing error: {e}")
return Error("Unable to process payment")
Two weeks later, we needed to add PayPal support. Tom spent three days:
class PayPalStrategyAdapter(AbstractPaymentProvider):
def __init__(self,
paypal_factory: IPayPalStrategyFactory,
config_chain: IConfigurationChain):
# Next sprint meeting: "I'm working on the PayPal integration"
pass
Bob tookby the end of the day:
def process_paypal_payment(self, total, user_email):
try:
paypal_result = paypal.create_payment(total)
if paypal_result.approved:
return Success(transaction_id=paypal_result.id)
return Error(paypal_result.reason)
except PayPalError as e:
log.error(f"PayPal error: {e}")
return Error("PayPal processing failed")
The difference is that Tom’s code was “clean” according to every best practice article. Bob’s code was clean because it told a story validate cart, calculate price, handle payment, manage errors.
The Extremes in Production: Two Ways to Write Unmaintainable Code
The Spaghetti Monster’s
These are the type of devs none willing to work with or touch their code. usuallu the belong to the “move fast and break things” crowd:
def process_order(data):
# Process payment or whatever
res = None
if data and data.get('items'):
# Calculate total I guess
tmp = 0
for x in data['items']:
if x.get('price'):
tmp += float(x['price'])
# Apply discount maybe?
if data.get('discount') and data['discount'] == 'SPECIAL':
tmp = tmp * 0.9
# Process payment somehow
if charge_card(data.get('card'), tmp): # Nobody knows where this function is
res = 'success'
# Update inventory probably
for x in data['items']:
update_inv(x.get('id')) # This sometimes works
# Send email if we feel like it
if data.get('email'):
send_mail(data['email'], 'Order confirmed maybe')
return res or 'failed for some reason'
# No tests because "it works on my machine", and not testable at all
# No docs because "the code is self-documenting"
# No type hints because "Python is dynamic"
# Changed by: Everyone
# Understood by: No one
This is what happens when YAGNI (You Ain’t Gonna Need It) becomes an excuse for ‘You Don’t Need to Think’ The kind of code that makes you wish for some abstraction - any abstraction - just to make sense of the chaos. These developers believe clean code means “it works,” documentation is for the weak, and maintaining code is tomorrow’s problem. Their variables are named ‘x’, ‘temp’, and ‘stuff’, their functions do seventeen different things based on the phase of the moon, and their error handling strategy is “it hasn’t crashed yet. chaning a single line can set everything on fire.
The Enterprise Astronaut’s Dream
Then Mr. “I Read All Design Patterns” Tom came to “fix” the chaos caused by “move fast and break things” Dave:
class AbstractOrderProcessingStrategyFactoryProvider(IOrderProcessingFactory):
def __init__(self,
payment_strategy_factory: IPaymentStrategyFactory,
inventory_chain_manager: IInventoryChainManager,
notification_pipeline: INotificationPipelineFactory,
validation_context: IValidationContextProvider):
self.payment_factory = payment_strategy_factory
self.inventory_manager = inventory_chain_manager
self.notification_pipeline = notification_pipeline
self.validation_context = validation_context
def process_order(self, order_context: IOrderContext) -> IOrderResult:
return (self.validation_context.create_validator()
.with_strategy(self.payment_factory.create_strategy())
.with_inventory_chain(self.inventory_manager.get_chain())
.with_notification(self.notification_pipeline.create_pipeline())
.validate()
.process()
.execute()
.get_result())
Both extremes miss the point entirely. One gives us code that works by accident, the other gives us architecture that works in theory. Neither gives us code we can actually maintain.
The Clean Solution: Writing Code That Makes Sense
After seeing those two extremes, let us look at what actual clean code looks like that tells a story without writing a novel of abstractions:
class OrderProcessor:
def __init__(self, payment_service, inventory_service, notifier):
self.payment_service = payment_service
self.inventory_service = inventory_service
self.notifier = notifier
def process_order(self, order):
"""Process a customer order from payment to delivery."""
if not self._validate_order(order):
return OrderResult.invalid()
try:
# Each step is clear, traceable, and has a single purpose
payment_result = self.payment_service.charge(order.payment_details)
if not payment_result.success:
return OrderResult.payment_failed(payment_result.error)
# Business logic is obvious and readable
items = self.inventory_service.reserve_items(order.items)
if not items.all_available:
self.payment_service.refund(payment_result.transaction_id)
return OrderResult.items_unavailable(items.unavailable)
# Clear error handling and user communication
self.notifier.send_confirmation(order.customer_id, order.details)
return OrderResult.success(order.id)
except Exception as e:
self._handle_error(e, order)
return OrderResult.system_error()
def _validate_order(self, order):
"""Simple validation with clear intent"""
return order.has_items and order.payment_details.is_valid
This code is clean because:
It tells a clear story about what it does, and each method has a single, obvious responsibility, on top of that dependencies are explicit and injected, error handling is straightforward, names are descriptive and meaningful. Comments explain “why” when needed, not “what”, business logic is easy to follow and complex operations are broken down into readable steps.
No excessive abstraction layers, no fancy patterns, no magic numbers, just code that the next developer can understand and modify without needing to decipher an architectural maze or untangle a ball of spaghetti. This hits the sweet spot between the extremes:
The Abstraction Tax
Every layer of abstraction comes with a cost. It iss like interest on a credit card it compounds, and before you know it, you are drowning in technical debt wearing a fancy “enterprise patterns” suit. Thus every abstraction we add is a tax we pay not just once, but every time we touch the code. Each layer is another cognitive load for developers, another potential bug hiding spot, another thing to maintain.
Think about what really happens when we “enterprise-ify” our code:
Hard to fix bugs
Good luck with bug fixing! tracing through seventeen layers of abstraction to find out why a button click isnt working. Tom’s logs:
ERROR: AbstractPaymentStrategyFactory encountered validation exception in chain
ERROR: IPaymentProvider validation failed at step 3
ERROR: Strategy initialization encountered unknown state
# What the hell is actually broken?
Bobs’s logs:
ERROR: PayPal error: API key is missing or invalid
# Fixed in 5 minutes by updating the config
The actual clean code clearly shines when a bug hits.
The Junior Developer Experience
New team members will spend their first month just drawing architecture diagrams trying to understand how everything connects. Let me tell you about Bob. Bob joined our team last month. Bob was eager, bright, and ready to contribute. Then he met our codebase:
# Day 1: Add a simple feature
def add_user_name_field():
# Step 1: Understand the AbstractUserFieldFactoryProvider
# Step 2: Implement IUserFieldStrategy
# Step 3: Configure FieldValidationChainFactory
# Step 4: Update DependencyInjectionConfiguration
# Step 5: Update resume, and LinkedIn profile.
Velocity Death
That simple feature request now takes days just to understand how to plug it into our “flexible” architecture.
Day 1-3: Understand how the abstractions work Day 4: Realize they don’t quite fit Day 5: Try to bend them to our will Day 6: Create three new interfaces Day 7: Write adapter classes Day 8: More adapter classes Day 9: Question career choices
but at least it iss enterprise-ready, right?
The Maintenance Nightmare
# The codebase six months later
class PaymentProcessor:
"""
Originally a simple payment processor.
Now a doctoral thesis in enterprise patterns.
Author: Multiple developers, all of whom have left
Last Modified: Nobody dares to touch it
Purpose: We think it processes payments, but who knows
"""
And the real thing is most of those clever abstractions we built for “future flexibility” we never actually needed them. Instead, they became prison cells, locking us into patterns that make even simple changes feel like architectural decisions. Those abstractions becomes burden on the team, and the cost of maintaining them is higher than the cost of writing the real business code.
The Path Forward: When to Abstract, When to Back Off
The Real Need for Abstractions
Not all abstractions are evil. The trick is knowing when they actually solve more problems than they create. One crucial case is protecting your codebase from third-party dependencies. Let’s see why and how.
Protecting Your Code From Third-Party Dependencies
When working with external services, like payment providers, you want to:
- Isolate third-party code from your business logic.
- Protect against API changes.
- Make testing easier.
- Keep your code clean from vendor-specific details.
Here is what it looks like in practice:
# Without abstraction, third-party code spreads through your codebase
def process_order(order):
# Now our order processing knows too much about Stripe
stripe_result = stripe.Charge.create(
amount=order.total,
currency='usd',
source=order.token,
metadata={'order_id': order.id}
)
if stripe_result.status == 'succeeded': # Stripe-specific status
# More Stripe-specific code...
A Clean Abstraction Example
Here’s how to properly isolate third-party code:
# Clean interface that represents YOUR domain
class PaymentProvider:
def process_payment(self, amount, payment_details) -> PaymentResult:
"""Process payment with any provider."""
pass
# Third-party details isolated in specific implementations
class StripeProvider(PaymentProvider):
def process_payment(self, amount, details):
try:
result = stripe.Charge.create(
amount=amount,
source=details.token
)
return PaymentResult(
success=result.paid,
transaction_id=result.id
)
except stripe.error.StripeError as e:
return PaymentResult(success=False, error=str(e))
class PayPalProvider(PaymentProvider):
def process_payment(self, amount, details):
try:
result = paypal.Payment.create(
paypal.Payment(amount=amount)
)
return PaymentResult(
success=result.state == "approved",
transaction_id=result.id
)
except paypal.error.PayPalError as e:
return PaymentResult(success=False, error=str(e))
When to Abstract (The Right Reasons)
- When isolating third-party code:
# Your business logic stays clean
def process_order(order, payment_provider: PaymentProvider):
result = payment_provider.process_payment(
amount=order.total,
details=order.payment_details
)
# No knowledge of Stripe or PayPal specifics here
- When you have repeated patterns:
# Multiple places using the same pattern
payment_provider.process_payment(amount) # Same interface, different providers
- When simplifying complex interactions:
# Complex provider-specific details hidden behind clean interface
result = payment_provider.refund(transaction_id) # Instead of provider-specific refund logic
When to Run Away (The Wrong Reasons)
# When you're future-proofing for imaginary scenarios
class AbstractButtonClickHandlerStrategyFactory:
"""
Just in case we need to swap out our button click implementation
(We never did and we will never do)
"""
# When the abstraction is harder to understand than the problem
def simple_task():
return AbstractTaskExecutionStrategyFactory()
.create_strategy()
.execute_task()
# All to return "Hello World"
The Decision Framework
Before adding an abstraction, ask:
def should_i_abstract_this():
if isolating_third_party_code:
return "Yes, protect your codebase"
if repeated_in_multiple_places:
return "Yes, reduce duplication"
if might_need_it_someday:
return "No, YAGNI"
if everyone_else_is_doing_it:
return "Stop reading Medium for a week"
Remember: Good abstractions solve real problems today, not imaginary problems tomorrow. They should make our code cleaner, not cleverer.
The Wake-Up Call
You know you’ve gone too far when:
- Your dependency injection configuration is longer than your actual code.
- Your class names need their own classes to manage their length.
- You need three architectural diagrams to explain a button click.
- Stack traces read like War and Peace.
- Your onion-layered architecture has more layers than an onion.
The New Rules
- If you cant explain your architecture without drawing diagrams, it is wrong.
- If your class names need a scrollbar, you have failed.
- If your abstraction needs its own abstraction, stop.
- If you’re “future-proofing,” you’re probably need to do something else.
- If you cant explain it to a junior developer in 10 minutes, you have probably gone too far.
- Ask yourself: “What is the real problem we are trying to solve here? How easy is it to understand? How easy is it to maintain? How easy is it to change? How easy to make collaboration easier?”
The Final Word
Remember: The best code is the code that gets the job done without making the next developer question their career choices. Everything else is just architectural theatrics. You know you have gone too far when your dependency injection configuration is longer than your actual code, your class names need a scrollbar, and your abstractions need their own abstractions. If you cant explain your system to a junior developer in 10 minutes, you’ve probably missed the point. The real questions are simple: What problem are we actually solving? How easy is it to understand? How easy is it to change? Because at the end of the day, clean code isnt about patterns or abstractions. It is about telling a story that another developer can understand when a bug hits.