“Clean code” is one of the most influential ideas in modern software development. It shaped how teams think about naming, function size, class responsibility, duplication, readability, and refactoring. Many of its core lessons remain valuable. Clear intent still matters. Unnecessary complexity is still a problem. Good structure still improves maintenance.
But strongly typed Scala projects expose the limits of applying classic clean code advice too literally. What reads as clean in an object-oriented, mutable, and mostly runtime-checked codebase may become awkward, verbose, or even misleading in Scala. This is not because Scala rejects readability. It is because Scala often moves clarity into different places: the type system, algebraic modeling, effect composition, and compiler-checked constraints.
In other words, code that looks less “clean” by traditional rules may be more precise, safer, and easier to evolve in a Scala codebase. The problem is not clean code itself. The problem is assuming that one style of cleanliness works equally well across very different programming models.
Readability in Scala is not only in the surface syntax
Classic clean code advice usually treats readability as something visible directly in the code’s surface form. Small methods, descriptive names, flat control flow, and minimal abstraction are often seen as the path to clarity. In Scala, that idea only tells part of the story.
A strongly typed Scala program often communicates its meaning through types as much as through method bodies. A return type like Either[DomainError, Order], IO[PaymentResult], or NonEmptyList[ValidationError] tells an experienced reader more than a series of defensive comments and imperative checks ever could. The code may look more abstract at first glance, but it is also more explicit about what can happen.
This creates a tension with traditional clean code rules. A Scala function may be slightly denser in syntax, yet far clearer in semantics because the type signature describes failure, effects, constraints, and valid states. If a team judges readability only by how simple the body looks, they may strip away the very type information that makes the system trustworthy.
“Prefer simple primitives” often leads to weaker domain models
A common habit in clean code culture is to avoid overengineering and keep things simple with primitives and straightforward data structures. In Scala, that instinct often causes real damage. Strongly typed projects benefit from replacing vague primitives with domain-specific types.
A String for email, a String for currency, a String for country code, and a String for order status may look simple, but they create a weak model. The compiler cannot distinguish them. Illegal states remain easy to construct. Validation becomes scattered. Bugs survive because the system does not encode enough meaning.
Scala encourages a different kind of cleanliness: making invalid states hard or impossible to represent. This often means introducing opaque types, value classes, enums, sealed traits, and smart constructors. To someone applying classic clean code mechanically, this may look like unnecessary indirection. In practice, it usually reduces complexity by moving ambiguity out of the business logic and into the type layer, where it can be checked once and reused everywhere.
Small functions are not always the same as clear composition
Traditional clean code often praises very small functions, ideally doing one thing and one thing only. That guideline sounds universal, but in Scala it can become misleading if applied without context.
Functional Scala code frequently expresses logic through composition rather than step-by-step narration. A pipeline involving transformations, validations, effect handling, and error accumulation may read naturally as a chain of combinators. Splitting every transformation into tiny named helpers can actually obscure the flow, especially if the helpers add little meaning beyond what the types and operators already express.
This does not mean long functions are automatically better. It means that in Scala, the unit of clarity is often the composition itself. Breaking a lawful transformation pipeline into many tiny wrappers may satisfy a clean code aesthetic while making the code harder to follow. The reader now has to jump across files or scroll through layers of names that do not clarify the domain any better than the original expression did.
“Avoid duplication” can become the wrong optimization
Another classic principle says duplication is bad and should be removed aggressively. In Scala, this advice needs more restraint. Not all duplication is equally harmful, and abstracting too early can produce extremely generic code that becomes harder to understand than the repeated logic it replaced.
This happens often in strongly typed systems. Teams notice similar workflows for different domains and try to unify them through type classes, higher-kinded abstractions, or generic service layers. Sometimes that is the right move. Often it is not. Two processes may look similar structurally while differing in business meaning, failure behavior, or lifecycle constraints. Forcing them into one abstraction can erase domain clarity and introduce accidental complexity.
Scala gives developers powerful tools for abstraction, which makes this risk even greater. A clean-looking generic solution may impress technically but leave future maintainers struggling to answer a basic question: what business operation is this code actually performing? In many Scala systems, a little duplication at the edge of the domain is cheaper than one elegant abstraction that nobody wants to touch.
Classic advice undervalues type-driven design
Many clean code traditions were shaped in languages where types offered limited expressive power. As a result, design advice often centers on object boundaries, naming discipline, test structure, and runtime behavior. Scala changes that balance because the type system itself becomes a major design tool.
In a strongly typed Scala project, some of the most important design decisions are not in class hierarchies or method comments. They are in whether you model alternatives with sealed traits, whether effects are explicit, whether optionality is represented honestly, whether errors are accumulated or short-circuited, and whether construction is validated at the boundary.
These decisions are not cosmetic. They determine how safe the code is to change. A project with rich, truthful types can feel unfamiliar to developers trained on traditional clean code patterns, especially if they are used to pushing complexity into services and keeping models thin. But in Scala, thin models often mean complexity is merely deferred until runtime.
Dependency injection and service patterns often become heavier than necessary
Classic clean code culture often treats dependency injection, service layers, and interface-based design as default signs of decoupling and testability. In Scala, those patterns are not always the cleanest choice. Many projects become more complex when they import enterprise object-oriented habits without adapting them to functional and type-driven design.
A deeply layered architecture full of traits, implementations, factories, and injected services may look organized on paper. In practice, it can obscure effect flow, duplicate wiring logic, and make local reasoning harder. Scala alternatives such as constructor-based composition, module values, tagless-final algebras, or direct effectful programs can be much clearer when used appropriately.
The issue is not that dependency injection is wrong. It is that strongly typed Scala already gives teams other ways to express modularity and substitution. If developers follow classic patterns by habit, they may end up with more ceremony than clarity.
What clean code should mean in Scala instead
Scala does not require rejecting clean code. It requires redefining it. In a strongly typed Scala project, clean code is not just code that reads smoothly line by line. It is code that models the domain honestly, makes effects visible, narrows invalid states, and uses the compiler as an ally rather than a passive checker.
That may result in code that looks more abstract than traditional clean code examples. It may use richer types, fewer mutable objects, more explicit effect boundaries, and denser composition. But if those choices reduce ambiguity and protect correctness, they are not a failure of cleanliness. They are a different form of it.
The best Scala teams usually stop asking, “Does this look like classic clean code?” and start asking better questions. Does this type tell the truth? Can invalid input leak too far into the system? Is this abstraction helping the domain or only reducing repetition? Does this composition preserve meaning? Will the compiler help the next person change this safely?
Cleanliness is contextual, not universal
The real lesson is that clean code is not a fixed visual style. It is a relationship between code, language, and problem domain. Advice that works well in one ecosystem can become shallow or even harmful in another. Strongly typed Scala projects expose this clearly because they shift much of the design burden from imperative structure to semantic modeling.
That shift changes what “clean” should look like. In Scala, clean code is often less about neat surfaces and more about honest constraints. Less about avoiding every abstraction and more about choosing the right one. Less about writing code that merely looks simple and more about building systems that remain correct, understandable, and changeable under pressure.
That is why classic clean code advice so often works poorly when imported unchanged into Scala. It is not because Scala is hostile to clarity. It is because real clarity in Scala lives deeper than style.

