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:

This chapter is your reference guide to the craft of writing clean code. It is structured into three parts for different situations:

  1. The Cheat Sheet: For when you’re in the middle of coding and need a high-speed reminder of a core principle.
  2. The Self-Review Checklists: For when you’ve finished a task and want to review your work with a professional, critical eye.
  3. 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.1 Deep Dive 1: The Art of Naming — Creating a Shared Vocabulary

Names are the bedrock of communication in code. Good naming is about creating a shared, consistent, and precise vocabulary that everyone on the team can understand.

  • Analogy: Building a Business Glossary As a Business Analyst, you know the importance of a glossary. Does the business say “Client,” “Customer,” or “Account”? You pick one, define it, and ensure everyone uses it consistently to avoid confusion. Coding is the same. If you have a class class Customer(val id: Int, ...) then other variables should be customerId, not clientID or cust_num. Consistency removes cognitive friction for the reader.

  • Example: From Technical Jargon to Business Language

    Before: Code with generic, computer-sciencey names 🤢

    // This function iterates over a list of items, filters them based on
    // a flag, and returns a new list.
    def filterList(list: List[Item], flag: Boolean): List[Item] = {
        list.filter(item => item.flag == flag)
    }

    This name is technically true, but it tells us nothing about the business domain.

    After: Code that speaks the language of the business ✅

    // This function selects invoices that have been processed and paid.
    def selectPaidInvoices(invoices: List[Invoice]): List[Invoice] = {
      invoices.filter(invoice => invoice.isPaid)
    }

    This version is immediately understandable to anyone on the team, technical or not. It connects directly to the business process. Always strive to name things in terms of the business problem you are solving.

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() and turnLightOff().

  • 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) {
      shippingService.shipOrder(order)
    }

    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 this if 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) {
          shippingService.shipOrder(this)
          this.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)
    order.ship() // We TELL the order to ship. We don't care how.

    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.