Arjan Codes reveals why blind clean code rules destroy software design
The Trap of Over-Decomposition
Software developers often treat Clean Code 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 Python Protocol 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.

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 Python Data Classes 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 Python 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 Python Protocol. 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.
- Python Protocol
- 25%· products
- Arjan Codes
- 13%· people
- Clean Code
- 13%· books
- Dependency Injection Container
- 13%· products
- Hostinger
- 13%· products
- Other topics
- 25%

Why “Clean Code” Often Creates Worse Designs
WatchArjanCodes // 26:27
On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!