Skip to main content

Command Palette

Search for a command to run...

Why Clean Code is Overrated And What Really Matters Instead

Updated
Why Clean Code is Overrated And What Really Matters Instead

I’m about to commit heresy in the software development world: Clean Code is overrated.

Before you dismiss this as clickbait heresy, let me share what I’ve learned from countless code reviews, failed projects, and production disasters. I have inherited pristine codebases with 100% test coverage that were impossible to modify, and I've maintained ugly PHP monoliths that processed millions of dollars in transactions without breaking a sweat.

The Clean Code movement, popularized by Robert Martin’s influential book, has created a generation of developers who prioritize aesthetic perfection over pragmatic problem-solving. It’s time we had an honest conversation about what really matters.

The Clean Code Obsession

Walk into any modern development team, and you’ll witness the ritualistic worship of clean code principles:

  • Functions must be short (but how short is too short?)

  • Variables need descriptive names (even if they make simple operations verbose)

  • Comments are code smells (because apparently code should be self-documenting)

  • Every abstraction must be justified (leading to analysis paralysis)

Don’t get me wrong, these aren’t bad principles. The problem is when they become dogma, when pull requests get blocked over method length while critical production issues wait in the backlog, or when teams spend sprint planning sessions debating code organization instead of solving actual user problems.

What We’re Getting Wrong

1. Perfectionism Over Progress

I’ve watched teams spend entire sprints refactoring perfectly functional code to meet arbitrary cleanliness standards. Meanwhile, critical features sat in the backlog, and customer complaints went unaddressed.

Clean code advocates often forget that working software is the primary measure of progress, not how elegant your abstractions look. A “dirty” function that solves a real problem is infinitely more valuable than a pristine codebase that doesn’t work.

2. Context Blindness

Clean code principles were largely developed for large, long-lived enterprise applications. But what about:

  • Rapid prototypes that need to validate ideas quickly?

  • One-off data processing scripts?

  • Research code exploring new algorithms?

  • Startup MVPs racing to market?

Applying the same rigorous standards across all contexts is like wearing a tuxedo to the beach, technically correct, but completely inappropriate.

3. The Abstraction Trap

Clean code emphasizes removing duplication and creating abstractions. This can lead to over-engineering, where simple solutions become complex hierarchies of interfaces and classes.

I learned this lesson the hard way when I inherited a “user notification system” from a colleague who had left the company. What should have been a simple email and SMS sender had been transformed into an elaborate architecture with notification factories, message builders, delivery strategies, and channel adapters. The system was only half-implemented, it could send basic emails but crashed on anything else.

I spent three days just trying to understand the flow: NotificationManagerFactory created a NotificationManager which used a MessageBuilderStrategy to create messages via a NotificationChannelAdapter. To add a simple SMS feature, I would have needed to implement four new interfaces and modify six existing classes.

Instead, I scrapped the entire thing and wrote a straightforward NotificationService class with sendEmail() and sendSMS() methods. It took two hours to write, worked perfectly, and the whole team could understand it instantly. Sometimes, a little duplication is better than a bad abstraction. (I wrote about this specific issue in more detail in my previous article: The DRY Trap: Why Duplication Isn't Your Worst Enemy)

I’ve seen five-line functions extracted into separate methods “for readability,” creating a maze of indirection that makes debugging a nightmare.

Example: Over-abstracted “Clean” Code

interface PaymentProcessorInterface
{
    public function process(PaymentRequest $request): PaymentResult;
}
class PaymentProcessorFactory
{
    public function create(string $type): PaymentProcessorInterface
    {
        return match($type) {
            'stripe' => new StripePaymentProcessor(),
            'paypal' => new PayPalPaymentProcessor(),
            default => throw new InvalidArgumentException('Unknown processor')
        };
    }
}
class PaymentService
{
    public function __construct(
        private PaymentProcessorFactory $factory,
        private PaymentValidator $validator,
        private PaymentLogger $logger
    ) {}

    public function processPayment(array $data): bool
    {
        $request = $this->createPaymentRequest($data);
        $this->validatePaymentRequest($request);
        $processor = $this->createProcessor($request->getType());
        $result = $this->executePayment($processor, $request);
        $this->logPaymentResult($result);

        return $result->isSuccessful();
    }

    private function createPaymentRequest(array $data): PaymentRequest { /* ... */ }
    private function validatePaymentRequest(PaymentRequest $request): void { /* ... */ }
    private function createProcessor(string $type): PaymentProcessorInterface { /* ... */ }
    private function executePayment(PaymentProcessorInterface $processor, PaymentRequest $request): PaymentResult { /* ... */ }
    private function logPaymentResult(PaymentResult $result): void { /* ... */ }
}

Pragmatic “Dirty” Code That Actually Works

class PaymentProcessor
{
    public function processPayment(array $data): bool
    {
        // Validate required fields
        if (empty($data['amount']) || empty($data['type']) || empty($data['token'])) {
            throw new InvalidArgumentException('Missing required payment data');
        }

        $amount = (float) $data['amount'];
        $type = $data['type'];
        $token = $data['token'];

        // Log attempt
        error_log("Processing {$type} payment for {$amount}");

        try {
            if ($type === 'stripe') {
                $stripe = new \Stripe\StripeClient(config('stripe.secret'));
                $result = $stripe->charges->create([
                    'amount' => $amount * 100, // Convert to cents
                    'currency' => 'usd',
                    'source' => $token,
                ]);
                $success = $result->status === 'succeeded';
            } elseif ($type === 'paypal') {
                $paypal = new PayPalHttpClient(new SandboxEnvironment(
                    config('paypal.client_id'),
                    config('paypal.secret')
                ));
                $request = new OrdersCaptureRequest($token);
                $response = $paypal->execute($request);
                $success = $response->statusCode === 201;
            } else {
                throw new InvalidArgumentException("Unsupported payment type: {$type}");
            }

            // Log result
            $status = $success ? 'SUCCESS' : 'FAILED';
            error_log("Payment {$status}: {$type} {$amount}");

            return $success;

        } catch (Exception $e) {
            error_log("Payment failed: " . $e->getMessage());
            return false;
        }
    }
}

The “clean” version has beautiful abstractions, but requires understanding 6+ classes to debug a payment failure. The “dirty” version puts everything in one place where you can actually see what’s happening.

What Actually Matters

If clean code isn’t the holy grail, what should we focus on instead?

1. Correctness First

Your code needs to work. Period. A messy function that handles edge cases properly is better than a clean function that fails silently. Focus on:

  • Robust error handling

  • Input validation

  • Expected behavior under all conditions

  • Comprehensive testing (not just unit tests)

Example: “Clean” Code That Looks Nice But Fails

class UserService
{
    public function createUser(CreateUserRequest $request): User
    {
        $user = $this->buildUserFromRequest($request);
        $this->persistUser($user);
        $this->sendWelcomeEmail($user);

        return $user;
    }

    private function buildUserFromRequest(CreateUserRequest $request): User
    {
        return new User($request->getName(), $request->getEmail());
    }

    private function persistUser(User $user): void
    {
        $this->userRepository->save($user);
    }

    private function sendWelcomeEmail(User $user): void
    {
        $this->emailService->sendWelcome($user);
    }
}

Robust Code That Actually Handles Reality

class UserService
{
    public function createUser(array $userData): array
    {
        // Validate input thoroughly
        $errors = [];

        if (empty($userData['name']) || strlen($userData['name']) < 2) {
            $errors[] = 'Name must be at least 2 characters';
        }

        if (empty($userData['email']) || !filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) {
            $errors[] = 'Valid email address required';
        }

        if (!empty($errors)) {
            return ['success' => false, 'errors' => $errors];
        }

        // Check if user already exists
        $existingUser = $this->findUserByEmail($userData['email']);
        if ($existingUser) {
            return ['success' => false, 'errors' => ['Email already registered']];
        }

        try {
            // Create user with proper error handling
            $userId = $this->database->insert('users', [
                'name' => trim($userData['name']),
                'email' => strtolower(trim($userData['email'])),
                'created_at' => date('Y-m-d H:i:s'),
                'status' => 'active'
            ]);

            if (!$userId) {
                return ['success' => false, 'errors' => ['Failed to create user']];
            }

            // Try to send welcome email, but don't fail user creation if email fails
            try {
                $this->sendWelcomeEmail($userData['email'], $userData['name']);
            } catch (Exception $e) {
                // Log email failure but continue - user was created successfully
                error_log("Welcome email failed for user {$userId}: " . $e->getMessage());
            }

            return [
                'success' => true,
                'user_id' => $userId,
                'message' => 'User created successfully'
            ];

        } catch (Exception $e) {
            error_log("User creation failed: " . $e->getMessage());
            return ['success' => false, 'errors' => ['System error occurred']];
        }
    }

    private function findUserByEmail(string $email): ?array
    {
        return $this->database->selectOne('users', ['email' => strtolower(trim($email))]);
    }
}

The first version follows clean code principles but will break in dozens of real-world scenarios. The second version is “messier” but actually works in production.

2. Performance When It Counts

Clean code can sometimes conflict with performance. Those beautifully abstracted layers might look nice, but they can introduce overhead that matters in critical paths.

Know when performance is crucial and when it isn’t. A banking transaction system needs different optimization than a content management dashboard.

Example: “Clean” Code That Kills Performance

class ProductService
{
    public function getProductsWithReviews(array $productIds): array
    {
        $products = [];

        foreach ($productIds as $id) {
            $product = $this->getProduct($id);
            $reviews = $this->getProductReviews($id);
            $rating = $this->calculateAverageRating($reviews);

            $products[] = $this->buildProductWithReviews($product, $reviews, $rating);
        }

        return $products;
    }

    private function getProduct(int $id): array
    {
        return $this->database->selectOne('products', ['id' => $id]);
    }

    private function getProductReviews(int $productId): array
    {
        return $this->database->select('reviews', ['product_id' => $productId]);
    }

    private function calculateAverageRating(array $reviews): float
    {
        if (empty($reviews)) return 0.0;

        $total = array_sum(array_column($reviews, 'rating'));
        return $total / count($reviews);
    }
}

This makes N+1 database queries and will crawl with even 100 products.

Performance-Focused “Dirty” Code

class ProductService
{
    public function getProductsWithReviews(array $productIds): array
    {
        if (empty($productIds)) {
            return [];
        }

        // Get all products in one query
        $placeholders = str_repeat('?,', count($productIds) - 1) . '?';
        $products = $this->database->query(
            "SELECT * FROM products WHERE id IN ({$placeholders})",
            $productIds
        );

        // Get all reviews in one query with pre-calculated ratings
        $reviews = $this->database->query("
            SELECT 
                product_id,
                AVG(rating) as avg_rating,
                COUNT(*) as review_count,
                GROUP_CONCAT(
                    JSON_OBJECT('id', id, 'rating', rating, 'comment', comment, 'created_at', created_at)
                ) as review_data
            FROM reviews 
            WHERE product_id IN ({$placeholders})
            GROUP BY product_id
        ", $productIds);

        // Index reviews by product_id for fast lookup
        $reviewsMap = [];
        foreach ($reviews as $review) {
            $reviewsMap[$review['product_id']] = [
                'avg_rating' => (float) $review['avg_rating'],
                'review_count' => (int) $review['review_count'],
                'reviews' => json_decode('[' . $review['review_data'] . ']', true)
            ];
        }

        // Combine data efficiently
        $result = [];
        foreach ($products as $product) {
            $productId = $product['id'];
            $reviewData = $reviewsMap[$productId] ?? [
                'avg_rating' => 0.0,
                'review_count' => 0,
                'reviews' => []
            ];

            $result[] = array_merge($product, $reviewData);
        }

        return $result;
    }
}

Less “clean” but goes from 1000+ database queries to just 2. Users will thank you.

3. Maintainability (But Not How You Think)

Yes, maintainability matters — but it’s not just about variable names and function length. True maintainability means:

  • Clear system architecture that’s easy to understand

  • Comprehensive documentation for complex business logic

  • Good test coverage that gives confidence when changing code

  • Consistent patterns that team members can follow

  • Reasonable complexity that matches the problem being solved

4. Team Velocity

The best code is code that allows your team to move fast. Sometimes this means taking shortcuts. Sometimes it means over-engineering for future flexibility. The key is making conscious decisions based on your specific context.

Ask yourself: “Will this clean code practice help my team deliver value faster, or will it slow us down?”

Example: Over-Engineered “Extensible” Code

abstract class ReportGenerator
{
    abstract protected function getData(): array;
    abstract protected function formatData(array $data): array;
    abstract protected function generateOutput(array $formattedData): string;

    public function generate(): string
    {
        $data = $this->getData();
        $formatted = $this->formatData($data);
        return $this->generateOutput($formatted);
    }
}
class SalesReportGenerator extends ReportGenerator
{
    protected function getData(): array
    {
        return $this->salesRepository->getMonthlySales();
    }

    protected function formatData(array $data): array
    {
        return array_map([$this, 'formatSalesRow'], $data);
    }

    protected function generateOutput(array $formattedData): string
    {
        return $this->csvFormatter->format($formattedData);
    }

    private function formatSalesRow(array $row): array
    {
        return [
            'month' => $row['month'],
            'sales' => number_format($row['total_sales'], 2)
        ];
    }
}

Simple Code That Gets Things Done

class ReportGenerator
{
    public function generateSalesReport(): string
    {
        // Get sales data
        $sales = $this->database->query("
            SELECT 
                DATE_FORMAT(created_at, '%Y-%m') as month,
                SUM(total) as total_sales
            FROM orders 
            WHERE status = 'completed' 
            AND created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
            GROUP BY DATE_FORMAT(created_at, '%Y-%m')
            ORDER BY month
        ");

        // Generate CSV
        $csv = "Month,Sales\n";
        foreach ($sales as $row) {
            $csv .= $row['month'] . ',' . number_format($row['total_sales'], 2) . "\n";
        }

        return $csv;
    }

    public function generateUserReport(): string
    {
        $users = $this->database->query("
            SELECT 
                DATE_FORMAT(created_at, '%Y-%m') as month,
                COUNT(*) as new_users
            FROM users 
            WHERE created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
            GROUP BY DATE_FORMAT(created_at, '%Y-%m')
            ORDER BY month
        ");

        $csv = "Month,New Users\n";
        foreach ($users as $row) {
            $csv .= $row['month'] . ',' . $row['new_users'] . "\n";
        }

        return $csv;
    }
}

When you only need two reports, the “extensible” approach wastes weeks of development time. Build what you need, when you need it.

5. Business Value

Never forget that code exists to solve business problems. The most elegantly crafted codebase is worthless if it doesn’t address real user needs.

Prioritize features that users actually want over internal code quality improvements that only developers care about.

A Balanced Approach

I’m not advocating for spaghetti code or abandoning all standards. Instead, I’m suggesting a more nuanced approach:

Be Pragmatic

  • Clean up code when it actually helps, not because a book says you should

  • Recognize that different parts of your system have different quality needs

  • Balance immediate needs with long-term maintainability

Consider Your Context

  • Early-stage startups need different practices than mature enterprises

  • Critical infrastructure requires different standards than internal tools

  • Team size and experience level should influence your approach

Focus on Outcomes

  • Measure success by user satisfaction, not code metrics

  • Prioritize fixes and features based on business impact

  • Remember that perfect is the enemy of good

Evolve Gradually

  • Improve code quality incrementally during normal development

  • Don’t halt feature development for massive refactoring projects

  • Let quality emerge naturally from good practices, not forced compliance

The Real Clean Code

Here’s what I think truly clean code looks like:

Clean code solves real problems efficiently. It’s as simple as the problem allows, but no simpler. It’s tested where testing matters, documented where documentation helps, and optimized where performance counts.

Clean code doesn’t follow arbitrary rules, it follows the principle of appropriateness. It’s code that your team can work with effectively, that your users can rely on, and that your business can build upon.

Sometimes this code will have long functions. Sometimes it will have comments explaining complex business logic. Sometimes it will have a little duplication to avoid over-abstraction. And that’s perfectly fine.

Conclusion

The Clean Code movement gave us valuable tools and principles, but like any tool, they need to be applied thoughtfully. Don’t let the pursuit of perfect code distract you from building perfect solutions.

The next time you find yourself spending hours debating code formatting while real problems go unsolved, ask yourself: “Am I optimizing for the right things?”

Your users don’t care if your functions are exactly 20 lines long. They care if your software works, performs well, and helps them accomplish their goals. Focus on that, and the rest will follow.

What’s your experience with clean code practices? Have you seen teams get too caught up in code aesthetics? please share your thoughts in the comments below.

More from this blog

P

Practical Web Development Tips & Guides - By Ayobami Omotayo

12 posts

TechBytesWithAyo by Ayobami Omotayo is your go-to hub for bite-sized insights on web development, blockchain, AI, and the latest tech trends, simplifying complex topics, one byte at a time.