11 A Pocket Guide to Clean Code in Scala
Welcome to one of the longest and, arguably, most important chapter in this book. Everything you’ve learned so far has taught you how to give instructions to a computer. This chapter will teach you how to communicate clearly with other people through your code.
This is the skill that separates a hobbyist from a professional craftsperson.
Why does it matter so much? Because code is read far more often than it is written. Your teammates, your future self, your boss—they will all need to understand the logic you’ve created. Clean, readable code leads directly to business value:
- It reduces bugs: Clear code is easier to reason about and has fewer hidden surprises.
- It accelerates teamwork: New team members can understand the code and start contributing faster.
- It lowers maintenance costs: Less time is spent deciphering old code, and more time is spent building new features.
This chapter is your reference guide to the craft of writing clean code. It is structured into three parts for different situations:
- The Cheat Sheet: For when you’re in the middle of coding and need a high-speed reminder of a core principle.
- The Self-Review Checklists: For when you’ve finished a task and want to review your work with a professional, critical eye.
- The Deep Dive: For when you have time to study the “why” behind the rules, with detailed analogies, examples, and professional insights.
Let’s begin.
11.1 Part 1: The Clean Code Cheat Sheet
Bookmark this section. It’s your quick, “at-a-glance” reference.
Category | Principle | Rule of Thumb & Quick Example |
---|---|---|
Naming | Reveal Intent | If you need a comment to explain the name, the name is wrong. val customerName not val s . |
Be Specific & Unambiguous | Use names you could say out loud in a business meeting. val overdueInvoices not val dataList . |
|
Use Standard Conventions | Booleans: isVerified , hasPermission . Functions: calculateSalesTax() . |
|
Be Consistent | If you use customer_id in one place, don’t use customerId in another. Create a shared vocabulary. |
|
Functions | Do One Thing (Single Responsibility) | If you describe your function using the word “and”, it’s doing too much. Break it up. |
Keep Them Small | Should fit on one screen without scrolling (ideally < 15 lines). Small functions are easy to name and test. | |
Don’t Repeat Yourself (DRY) | If you copy-paste code, you are creating future maintenance work. Extract it into its own function. | |
Avoid Flag Arguments | A boolean flag (doExtraStep: Boolean) means the function does two things. Create two separate functions instead. |
|
Comments | Explain “Why”, Not “What” | The code explains what it does. A comment should explain why it does it that way (e.g., a business trade-off). |
Good Code is Self-Documenting | Your first goal is to make the code so clear that it doesn’t need comments. | |
Delete “Zombie” Code | Don’t leave commented-out code in the codebase. That’s what Git is for. It’s noise. | |
Simplicity | Avoid “Magic Values” | Don’t use unexplained, hardcoded values. price * salesTaxRate not price * 0.07 . |
YAGNI (You Ain’t Gonna Need It) | Solve today’s problem simply. Don’t add complexity for a hypothetical future you can’t predict. | |
Principle of Least Astonishment | Your code should behave in a way that surprises the reader the least. | |
Structure | Tell, Don’t Ask | Tell objects to do work; don’t pull their internal data out to work on it yourself. order.ship() not if(order.isReady()){...} . |
Encapsulate What Varies | Hide implementation details that are likely to change behind a stable interface. | |
Errors | Use Option for Expected Absence |
null is a bug waiting to happen. An Option[T] is an honest and safe way to represent a missing value. |
11.2 Part 2: The Self-Review Checklists
Before you mark a task as “done,” take on the role of your own quality assurance engineer. These checklists provide the questions a senior developer would ask during a code review.
11.2.1 Checklist 1: The Five-Minute Function Review
(Run through this for every new function you write)
11.2.2 Checklist 2: The “Before You Commit” Professionalism Review
(Run through this before you save your work to the team’s repository)
11.3 Part 3: The Deep Dive — Principles, Analogies, and Examples
This is your reference library. When you want to truly understand the why behind a principle from the cheat sheet, find the corresponding section here for a detailed explanation.
11.3.2 Deep Dive 2: Crafting Perfect Functions — A Masterclass in Responsibility
A function is a self-contained unit of work. The best functions are like specialized, perfectly crafted tools.
Principle Focus: Avoid Flag Arguments A boolean “flag” passed into a function is a major code smell. It’s a sign that your function is doing more than one thing, and the caller has to peek inside to know which path will be taken.
Analogy: A Light Switch vs. a Dimmer with a Pull-Chain A clean function is a simple light switch: it does one thing, like
turnLightOn()
. A function with a flag argument is like a complex light fixture with a dimmer dial and a pull-chain. To use it, you have to know the current state: “If the pull-chain is down, the dimmer works, but if it’s up, the light is off regardless of the dimmer.” It’s confusing and error-prone. It’s better to have two simple switches:turnLightOn()
andturnLightOff()
.Example: Refactoring a Function with a Flag
Before: One function trying to be both a draft and final report generator 🤢
def generateReport(sales: List[Sale], isFinalVersion: Boolean): String = { val reportHeader = if (isFinalVersion) { "** OFFICIAL SALES REPORT **" } else { "** DRAFT SALES REPORT **" } val reportBody = createBody(sales) val reportFooter = if (isFinalVersion) { s"Generated on ${java.time.LocalDate.now}" } else { "--- For internal use only ---" } s"$reportHeader\n$reportBody\n$reportFooter" }
After: Two separate, honest functions. No flags needed. ✅
def generateDraftReport(sales: List[Sale]): String = { val header = "** DRAFT SALES REPORT **" val body = createBody(sales) val footer = "--- For internal use only ---" s"$header\n$body\n$footer" } def generateOfficialReport(sales: List[Sale]): String = { val header = "** OFFICIAL SALES REPORT **" val body = createBody(sales) val footer = s"Generated on ${java.time.LocalDate.now}" s"$header\n$body\n$footer" }
The two new functions are simpler, have no internal branching on a flag, and their names perfectly describe what they do. The code is now honest and clear.
11.3.3 Deep Dive 3: Tell, Don’t Ask — Respecting Object Boundaries
This is a more advanced Object-Oriented principle that leads to much cleaner systems.
Principle: Instead of asking an object for its data and then making decisions based on that data, you should tell the object what you want it to do and let it handle the internal logic itself. This respects encapsulation and moves behavior into the objects that own the data.
Analogy: Ordering at a Restaurant
- Asking (Bad): You walk into the kitchen, inspect the inventory of dough, sauce, and cheese, and if all ingredients are available, you start making the pizza yourself. This is intrusive and you need to know all the details of the kitchen’s operations.
- Telling (Good): You sit at your table and tell the waiter, “I’d like a pizza.” The waiter (the interface) takes your request, and the kitchen (the object) handles all the internal logic of checking inventory and preparing the meal. You don’t need to know the details; you trust the object to do its job.
Example: Moving Logic Inside the Object
Before: “Asking” the Order object for its status and acting upon it 🤢
// In our main application logic... val order = findOrderById(123) // We ASK for the status, then WE make the decision. if (order.status == "PAID" && order.itemsInStock == true) { .shipOrder(order) shippingService}
The problem here is that our main application logic now has to know all the rules about what makes an order “shippable.” If a new rule is added (e.g.,
isInternationalShippingApproved
), we have to find and change thisif
statement.After: “Telling” the Order object to ship itself ✅
// Our new Order class class Order(..., private val shippingService: ShippingService) { var status: String = "PENDING" var itemsInStock: Boolean = true // The behavior lives inside the object that owns the data! def ship(): Unit = { if (this.status == "PAID" && this.itemsInStock) { .shipOrder(this) shippingServicethis.status = "SHIPPED" } else { println("Order cannot be shipped in its current state.") } } } // In our main application logic, it's now beautifully simple: val order = findOrderById(123) .ship() // We TELL the order to ship. We don't care how. order
All the business logic for what “shippable” means is now encapsulated within the
Order
class itself. Our main logic is cleaner, and our system is more robust and easier to maintain.