Arjan Codes reveals why blind clean code rules destroy software design

The Trap of Over-Decomposition

Software developers often treat

as a rigid checklist: small classes, short methods, and abstractions everywhere. However,
Arjan Codes
warns that applying these rules blindly leads to over-decomposition. This creates a design that looks tidy on the surface but hides a "huge monster in the closet." The problem arises when you optimize for smallness rather than cohesion.

In the initial example, a sales report script used a

and a
Dependency Injection Container
for dependency injection. While these are technically advanced patterns, they served no functional purpose, merely masking a messy 200-line "run" method that handled loading, filtering, and exporting simultaneously. Real clean code isn't about the number of lines; it is about making the reasons for change visible and grouping logic that belongs together.

Arjan Codes reveals why blind clean code rules destroy software design
Why “Clean Code” Often Creates Worse Designs

Refactoring the Fake Abstraction

To begin the cleanup, we must strip away abstractions that solve imaginary problems. If a container exists just to instantiate one class that is never swapped out, it is dead weight. By deleting the ReportService protocol and the Container class, we bring the logic back to the main function where it can be directly controlled.

Next, we introduce a

for configuration. Instead of passing five or six separate arguments through every function, we group them into a cohesive ReportConfig object. This allows for sensible defaults—like UTF-8 encoding or comma delimiters—while making the settings explicit and easy to modify in one location.

@dataclass(frozen=True)
class ReportConfig:
    country: str = "Netherlands"
    min_revenue: float = 10.0
    allow_negative: bool = False
    delimiter: str = ","
    encoding: str = "utf-8"

Making the Pipeline Explicit

One of the most significant design improvements involves breaking the monolithic run method into a clear, linear pipeline: Load → Summarize → Export. By separating these concerns into pure functions, we gain massive flexibility and efficiency.

For instance, by moving the loading logic out of the core processing function, we can load the data once and run multiple summaries against it without re-reading the file from the disk. We also introduce a TypeAlias for our data structures to improve readability without the overhead of heavy class hierarchies.

from typing import TypeAlias

Data: TypeAlias = list[dict[str, str]]

def load_data(source: Path, config: ReportConfig) -> Data:
    # File loading logic here
    return data

Modeling Results with Cohesion

Instead of letting the summary data float around as raw numbers, we move behavior into the Summary object itself. If you need to convert a summary to text or JSON, that logic belongs to the summary, not the exporter. This shift moves the "how" of the report into the object that owns the "what."

We also centralize business logic. By creating an internal is_valid helper within the summarization function, we isolate the filtering rules (e.g., checking revenue thresholds or refund status) from the aggregation logic. This makes the primary loop significantly cleaner and focuses the summarize function on its actual job: calculating totals.

def summarize(data: Data, config: ReportConfig) -> Summary:
    def is_valid(row: dict) -> bool:
        return float(row["revenue"]) >= config.min_revenue

    valid_rows = [row for row in data if is_valid(row)]
    revenue_sum = sum(float(row["revenue"]) for row in valid_rows)
    return Summary(count=len(valid_rows), revenue_sum=revenue_sum)

Practical Examples and Syntax Notes

This approach shines when extending the system. To add a JSON export, we simply define an export_json function. Because our pipeline is explicit, we don't have to touch the loading or summarizing code. We just plug the new exporter into our main execution flow.

When using

for these patterns, utilize Path objects from pathlib rather than strings to handle file system operations more robustly. Additionally, the asdict utility from the dataclasses module is perfect for quickly converting your cohesive objects into formats suitable for json.dumps while maintaining control over the final output structure.

Tips and Gotchas

Avoid the temptation to abstract early. Wait until you have at least two or three different implementations before reaching for a

. The most common mistake is "hiding" behavior inside services, which makes debugging difficult. High cohesion means things that change together stay together; it doesn't mean every function has to be three lines long. Focus on visibility and meaningful boundaries to keep your code truly maintainable.

4 min read