Software craft · Cliff notes
Clean Code
A concise guide to the habits that make code easier to read, test, and change.
- Clean Code
- Naming
- Functions
- Testing
Based on Robert C. Martin's Clean Code. Read the short points by section, then expand any one for a better example, a costly one, and why each matters. Choose a language in any point to update all the examples. Treat the rules as prompts for judgment rather than fixed limits.
Hold Shift while opening a point to keep other examples open.
1. Names and intent
1.1 Reveal intent in names. Name the reason a value exists, not just its shape.
A reader should not have to trace assignments to discover what a value means.
Better example
let overdueInvoices = invoices.filter { $0.dueDate < today }Why it helps. overdueInvoices tells the reader what the filter produced without making them inspect the predicate.
Costly example
let list = invoices.filter { $0.dueDate < today }Why it hurts. list describes the container but hides the business meaning of its contents.
Use the domain term the next reader would search for.
1.2 Avoid misleading names. Do not promise one structure or meaning and deliver another.
A misleading label creates a false mental model even when the implementation works.
Better example
let accountsByID: [UUID: Account] = [:]Why it helps. accountsByID announces both the values and the lookup key.
Costly example
let accountList: [UUID: Account] = [:]Why it hurts. accountList suggests an ordered list, while the value is a dictionary keyed by UUID.
Names are a contract with the reader.
1.3 Use one vocabulary. Choose one word for each concept across the codebase.
Inconsistent synonyms make engineers wonder whether two operations differ.
Better example
func loadOrders() async throws -> [Order] func loadInvoices() async throws -> [Invoice]Why it helps. load means the same kind of retrieval for orders and invoices, so differences are easier to spot.
Costly example
func fetchOrders() async throws -> [Order] func retrieveInvoices() async throws -> [Invoice]Why it hurts. fetch and retrieve look like different operations even if they follow the same contract.
Keep differences in names meaningful.
1.4 Give useful context. Add context where it clarifies; avoid repeating the type or module.
A short name is only clear within the right scope.
Better example
struct Order { let customerEmail: String }Why it helps. Inside Order, customerEmail is specific enough without repeating the enclosing type or value type.
Costly example
struct Order { let orderCustomerEmailAddressString: String }Why it hurts. The long property name repeats context already provided by Order and the declared type, burying the useful concept.
Let surrounding types provide the rest of the story.
1.5 Replace magic values. Give domain thresholds names so their meaning is searchable.
An unexplained number can be correct today and impossible to update consistently tomorrow.
Better example
private let maximumLoginAttempts = 3 if attempts >= maximumLoginAttempts { lockAccount() }Why it helps. A named threshold gives the rule one searchable home and tells reviewers what three means.
Costly example
if attempts >= 3 { lockAccount() }Why it hurts. The literal three gives no clue whether it represents retries, elapsed time, or a policy limit.
A named constant explains why the number exists.
1.6 Name types and actions precisely. Use nouns for concepts and verbs for behavior.
A name such as Manager or process gives little clue about ownership or outcome.
Better example
struct OrderValidator { func validate(_ order: Order) throws { /* … */ } }Why it helps. OrderValidator and validate identify both the responsibility and the action.
Costly example
class DataManager { func process(_ order: Order) { /* … */ } }Why it hurts. DataManager and process are broad enough to hide almost any behavior.
Specific names make responsibilities easier to see.
2. Functions and control flow
2.1 Give each function one purpose. A function should tell one coherent story at one level.
Mixed business decisions and implementation details are harder to change safely.
Better example
func submit(_ order: Order) async throws { try validate(order) try await orderGateway.save(order) }Why it helps. submit reads as a sequence of domain steps while validation and persistence have their own homes.
Costly example
func submit(_ order: Order) async throws { guard order.total > 0 else { return } let data = try JSONEncoder().encode(order) try await client.post(data, to: "/orders") }Why it hurts. The function mixes eligibility, serialization, and network details, giving it several reasons to change.
Extract named steps when they clarify the main flow.
2.2 Group related inputs. When arguments travel together, model the concept they form.
A growing parameter list invites swapped values and unclear call sites.
Better example
let window = ScheduleWindow( start: date, end: endDate, timeZone: zone, calendar: calendar ) schedule(window)Why it helps. ScheduleWindow groups values that form one scheduling concept and makes the call site harder to scramble.
Costly example
schedule(start: date, end: endDate, zone: zone, calendar: calendar)Why it hurts. Four loose arguments make it easy to swap related values and hard to see their shared invariant.
A value type is useful when it has a real domain meaning.
2.3 Separate commands from queries. Make state changes visible at the call site.
A read-like name that writes data surprises callers and complicates tests.
Better example
func isEligible(_ user: User) -> Bool { user.age >= 18 } func recordEligibilityCheck(for user: User) { database.markChecked(user.id) }Why it helps. isEligible only answers a question; recording the check has an explicit command name.
Costly example
func isEligible(_ user: User) -> Bool { database.markChecked(user.id) return user.age >= 18 }Why it hurts. Calling isEligible unexpectedly writes to the database, so a read has a hidden effect.
A query should answer; a command should act.
2.4 Flatten the main path. Use early exits when they make the success path easy to scan.
Deep nesting makes conditions and outcomes harder to connect.
Better example
guard let user, user.isActive, let email = user.email else { return } send(to: email)Why it helps. An early exit leaves the successful action visible after one set of preconditions.
Costly example
if let user = user { if user.isActive { if let email = user.email { send(to: email) } } }Why it hurts. Three nested conditions push the useful action to the right and make the exit cases hard to scan.
Keep the important path prominent.
2.5 Remove knowledge duplication. Put a business rule in one authoritative place.
The same rule in multiple flows can drift over time.
Better example
struct ShippingPolicy { static let freeThreshold = 50 static func isFree(total: Decimal) -> Bool { total >= freeThreshold } }Why it helps. ShippingPolicy owns the threshold and the decision, so one policy change reaches every caller.
Costly example
let canShip = order.total >= 50 let qualifiesForFreeShipping = cart.total >= 50Why it hurts. The value fifty is duplicated in separate flows and can drift when the shipping rule changes.
Deduplicate shared knowledge, not every similar line.
2.6 Choose branching deliberately. A closed enum can be clearer than a hierarchy of tiny subclasses.
The goal is to keep variation local and understandable.
Better example
enum PaymentMethod { case card(CardDetails) case cash } switch paymentMethod { case .card(let details): payWithCard(details) case .cash: recordCashPayment() }Why it helps. The enum names the closed set of payment methods and makes the switch exhaustive.
Costly example
if kind == "card" { /* … */ } else if kind == "cash" { /* … */ } // Repeated in several filesWhy it hurts. Repeated string comparisons scatter the same branching rule and allow unhandled spellings.
Use exhaustive switches for closed alternatives; use polymorphism when extension is genuinely needed.
3. Comments and formatting
3.1 Let code explain the how. Replace narration with names and structure.
A comment that repeats code adds reading without adding understanding.
Better example
let total = price + taxPolicy.tax(for: price)Why it helps. taxPolicy.tax names the rule and can carry its real calculation, making the line self-explanatory.
Costly example
// Add tax to the price let total = price + price * 0.08Why it hurts. The comment only narrates the arithmetic and cannot explain why the rate is 0.08.
Make the operation explain itself.
3.2 Document the why. Preserve non-obvious constraints and decisions.
Some intent cannot be inferred from implementation alone.
Better example
// The provider may serve stale reads immediately // after an update; retry after its consistency window. try await Task.sleep(for: .seconds(2))Why it helps. The comment records the provider constraint that justifies an otherwise suspicious delay.
Costly example
// Wait two seconds try await Task.sleep(for: .seconds(2))Why it hurts. Wait two seconds repeats the code and leaves the important reason undocumented.
Explain the reason a future maintainer might otherwise remove.
3.3 Keep formatting consistent. Arrange related code so the file reads top to bottom.
Scanning becomes faster when declaration order and whitespace signal structure.
Better example
func load() { decode() } func save() { persist() } private func decode() { /* … */ } private func persist() { /* … */ }Why it helps. Related public operations appear together, with private details below them.
Costly example
func save() { persist() } private func encode() -> Data { /* … */ } func load() { decode() }Why it hurts. Interleaving a private helper with public operations makes the file harder to scan.
Use team conventions; readability matters more than a fixed line width.
3.4 Delete stale and dead commentary. Update constraints when behavior changes and remove commented-out code.
A false comment can send the next engineer toward the wrong fix.
Better example
// The provider recommends at most three // attempts to avoid duplicate processing. retry(maxAttempts: 3)Why it helps. The current comment explains the retry cap and matches the code that follows.
Costly example
// Retry forever until the server responds. // oldRetryLoop() retry(maxAttempts: 3)Why it hurts. The old comment contradicts the three-attempt cap, and commented-out code adds noise.
Version control preserves old code; comments should describe current intent.
4. Objects, data, and boundaries
4.1 Protect invariants. Expose behavior instead of public mutable internals.
Callers should not be able to leave a domain object in an impossible state.
Better example
struct Wallet { private(set) var balance: Decimal mutating func withdraw(_ amount: Decimal) throws { guard amount <= balance else { throw WalletError.insufficientFunds } balance -= amount } }Why it helps. Wallet checks the withdrawal and protects balance from arbitrary external mutation.
Costly example
struct Wallet { var balance: Decimal } wallet.balance = -100Why it hurts. Any caller can make balance negative without passing through a domain rule.
Let the owning type enforce its rules.
4.2 Avoid collaborator chains. Ask an owner for the answer instead of navigating its internals.
Long property chains couple a feature to several object shapes.
Better example
let fee = pricing.fee(for: order)Why it helps. Pricing answers the fee question without exposing the structure of customer and account objects.
Costly example
let fee = order.customer.account.plan.feeWhy it hurts. The chain ties fee calculation to several internal relationships that may change independently.
Move decisions toward the abstraction that owns them.
4.3 Keep plain data at boundaries. Immutable transfer values can be simpler than behavior-rich objects.
Serialization and external APIs often need explicit, stable data shapes.
Better example
struct WeatherDTO: Decodable { let temperatureCelsius: Double } let forecast = Forecast(dto: response)Why it helps. An immutable DTO represents transport data; conversion into Forecast gives domain behavior a separate home.
Costly example
final class WeatherResponse { var temp: Double = 0 func calculateForecast() { /* … */ } }Why it hurts. The mutable response object mixes a wire format with forecast logic and has unclear ownership.
Convert transport data into domain behavior at a clear boundary.
4.4 Wrap vendor assumptions. Translate third-party APIs once, near the edge.
A vendor change should have a small, testable blast radius.
Better example
protocol Notifier { func send(_ message: Message) async throws } struct VendorNotifier: Notifier { func send(_ message: Message) async throws { try await sdk.send(message, channel: "push-v2") } }Why it helps. The adapter contains the vendor channel name, so callers depend on a small Notifier contract.
Costly example
// Repeated throughout the app sdk.send(message, channel: "push-v2")Why it hurts. Every call site knows the SDK and its channel string, enlarging the impact of a vendor change.
Add a boundary when it protects a real dependency.
4.5 Write learning tests at boundaries. Capture the behavior you rely on from an external library.
A focused test detects changed assumptions before they spread through the app.
Better example
func testVendorRejectsInvalidDates() { let result = sdk.parseDate("not-a-date") XCTAssertNil(result) }Why it helps. The focused test records the exact SDK behavior the application relies on for invalid input.
Costly example
// Assume the SDK rejects invalid dates. let date = sdk.parseDate(raw)Why it hurts. The assumption is only a comment, so an SDK update can silently change behavior.
Test important vendor behavior, not the vendor’s entire implementation.
5. Errors and absence
5.1 Choose an honest failure contract. Represent expected absence separately from failure.
Callers need to distinguish an unavailable item from a failed operation.
Better example
func loadUser(id: UUID) throws -> User? { try database.fetch(id) // nil means not found }Why it helps. An absent result means the user was not found, while a database failure still reaches the caller.
Costly example
func loadUser(id: UUID) -> User? { try? database.fetch(id) // errors vanish }Why it hurts. Catching every error as an absent result makes a broken database look like a missing user.
Do not hide operational failures behind an absent value.
5.2 Add context to errors. Include the failed operation and preserve the underlying cause.
An isolated low-level message is hard to act on in production.
Better example
catch { throw SyncError.uploadFailed( documentID: document.id, underlying: error ) }Why it helps. The new error identifies the document and retains the original failure for diagnosis.
Costly example
catch { throw AppError.failed }Why it hurts. A generic failed error discards the operation, affected document, and underlying cause.
Give the caller the context needed to respond.
5.3 Model normal absence clearly. Return an empty collection for no matches, and reserve absence for a missing single value.
Sentinel values and forced unwraps create surprising failure paths.
Better example
func search(_ text: String) -> [Item] { matches // [] is a valid result }Why it helps. An empty collection is a valid zero-match result that callers can iterate safely.
Costly example
func search(_ text: String) -> [Item]? { matches.isEmpty ? nil : matches }Why it hurts. An absent collection adds an extra state even though there was no failure or missing search.
Let the type express the real contract.
6. Tests and evolution
6.1 Test observable behavior. Describe a scenario and its outcome, not private steps.
Tests tied to internals resist safe refactoring.
Better example
func testExpiredCouponDoesNotDiscountTotal() { let total = checkout.total(using: expiredCoupon) XCTAssertEqual(total, 100) }Why it helps. The test describes the expired-coupon rule through the total the caller receives.
Costly example
func testCallsHelperTwice() { XCTAssertEqual(mock.helperCalls, 2) }Why it hurts. A helper call count tests the current implementation, so a safe refactor can break the test.
A test should explain a rule a user or caller relies on.
6.2 Keep tests independent. Each test creates its own state and can run in any order.
Shared mutable fixtures cause intermittent, hard-to-reproduce failures.
Better example
func testCartContainsAddedItem() { var cart = Cart() cart.add(item) XCTAssertEqual(cart.count, 1) }Why it helps. The test creates its own Cart and can run alone or in any order.
Costly example
func testA() { sharedCart.add(item) } func testB() { XCTAssertEqual(sharedCart.count, 1) }Why it hurts. The second test depends on the first test mutating sharedCart, making order part of the result.
A test should pass alone as well as in the suite.
6.3 Keep feedback fast and repeatable. Replace uncontrolled time and network dependencies in unit tests.
Fast, deterministic tests support frequent safe changes.
Better example
func testExpiry() { let clock = TestClock(now: expiryDate) XCTAssertTrue(token.isExpired(at: clock.now)) }Why it helps. The test controls time directly and checks expiry without waiting a minute.
Costly example
func testExpiry() async throws { try await Task.sleep(for: .seconds(60)) XCTAssertTrue(token.isExpired) }Why it hurts. A real sleep makes the test slow and sensitive to scheduling.
Control the environment when testing a rule.
6.4 Assert one coherent outcome. Multiple checks can describe one behavior.
The goal is a focused failure, not a literal assertion count.
Better example
func testDiscountedCheckoutTotal() { let receipt = checkout(discount: .tenPercent) XCTAssertEqual(receipt.subtotal, 100) XCTAssertEqual(receipt.discount, 10) XCTAssertEqual(receipt.total, 90) }Why it helps. Several assertions describe one receipt calculation and make its parts understandable.
Costly example
func testCheckout() { XCTAssertEqual(total, 90) XCTAssertEqual(emailCount, 1) XCTAssertEqual(stock, 4) }Why it hurts. Total, email, and stock are separate behaviors; one failure does not identify the broken rule clearly.
Keep the test focused on one idea.
6.5 Practice red–green–refactor. Use a failing behavior test to guide a small change.
Short feedback loops prevent cleanup from drifting away from required behavior.
Better example
// Red: describe the missing discount rule. // Green: implement the smallest correct change. // Refactor: clarify names and remove duplication.Why it helps. A failing rule, minimal implementation, and cleanup keep each step tied to feedback.
Costly example
// Implement the whole feature, then write // tests for the methods that already exist.Why it hurts. Writing tests only after the whole feature encourages checks that mirror the code already written.
Keep each step small enough to understand.
6.6 Cover failure and edge cases. Test the contract where inputs and dependencies are awkward.
Happy-path-only tests miss the conditions most likely to expose ambiguity.
Better example
func testTransferRejectsInsufficientFunds() { let result = transfer(100, from: walletWith50) XCTAssertEqual(result, .insufficientFunds) }Why it helps. The test states what insufficient funds must return, protecting an important boundary.
Costly example
func testTransfer() { XCTAssertTrue(transfer(10).succeeded) }Why it hurts. A successful transfer alone says nothing about how the operation handles invalid amounts.
Boundary tests teach future readers what the system promises.
7. Classes, systems, and emergence
7.1 Give types focused reasons to change. Separate policies that evolve independently.
A large type that mixes unrelated concerns makes every change risky.
Better example
struct TaxCalculator { /* … */ } struct InvoiceRenderer { /* … */ } struct ReceiptSender { /* … */ }Why it helps. Tax, rendering, and sending can evolve independently without editing one central class.
Costly example
final class OrderManager { func calculateTax() { /* … */ } func renderPDF() { /* … */ } func sendEmail() { /* … */ } }Why it hurts. OrderManager combines three policies that can change for unrelated reasons.
Split by responsibility, not arbitrary file size.
7.2 Separate construction from use. Wire dependencies at an edge; keep domain logic focused.
Hidden construction makes a unit difficult to test or replace.
Better example
final class Checkout { let gateway: PaymentGateway init(gateway: PaymentGateway) { self.gateway = gateway } }Why it helps. Checkout receives a gateway explicitly, allowing a test or new provider to supply another implementation.
Costly example
final class Checkout { let gateway = LivePaymentGateway() }Why it hurts. Checkout constructs the live gateway itself, hiding a network dependency inside domain code.
Make important dependencies visible.
7.3 Grow architecture through evidence. Add boundaries when change pressure reveals them.
Speculative layers create maintenance work before they protect anything.
Better example
struct DisplayName { static func forUser(_ user: User) -> String { "\(user.firstName) \(user.lastName)" } }Why it helps. A small named function solves the present display rule without a framework of factories.
Costly example
protocol UserNameFormatterFactoryProvider { func make() -> UserNameFormatterFactory }Why it hurts. The provider factory hierarchy adds indirection before any variation has been demonstrated.
Prefer the simplest design that meets today’s needs.
7.4 Refine in safe increments. Keep behavior verified while improving structure.
Small steps make regressions easier to locate and undo.
Better example
// 1. Characterize existing behavior. // 2. Extract parsing behind a boundary. // 3. Switch one caller, then repeat.Why it helps. Characterization, extraction, and gradual caller migration keep behavior observable between changes.
Costly example
// Rewrite parser, storage, and UI in one PR replaceEverything()Why it hurts. Replacing parser, storage, and UI together makes a regression difficult to locate.
Make it work, make it clear, and keep tests green.
7.5 Keep a type cohesive. Its methods and state should serve the same concept.
A grab bag of unrelated operations has no stable reason to change.
Better example
struct AvatarEditor { /* image behavior */ } struct TaxCalculator { /* tax policy */ }Why it helps. Image editing and tax policy each have a type with one clear purpose.
Costly example
final class ProfileService { var avatar: Image? func cropImage() { /* … */ } func calculateTax() { /* … */ } }Why it hurts. ProfileService contains unrelated image and tax behavior, so its name predicts neither.
Cohesion is about shared purpose, not a tiny method count.
7.6 Use the simple design priorities. Keep tests passing, remove real duplication, express intent, then trim excess structure.
Minimizing class count first can hide responsibilities and raise change cost.
Better example
// Separate only the concepts that have // different rules or reasons to change. struct Invoice { /* … */ } struct TaxPolicy { /* … */ }Why it helps. Invoice and TaxPolicy name distinct concepts while avoiding extra scaffolding.
Costly example
// One giant type avoids extra classes. final class Everything { /* … */ }Why it hurts. One Everything class minimizes the class count but obscures rules and reasons to change.
Simplicity means easy to understand and change.
7.7 Leave the code a little better. Improve a nearby name, test, or boundary during normal work.
Incremental care prevents a backlog of avoidable confusion.
Better example
let amountDue = invoice.amountDue // Add a regression test for the repaired rule.Why it helps. A clearer local name and regression test improve the touched area while preserving behavior.
Costly example
// Fix the bug; preserve the misleading name. let x = invoice.amountDueWhy it hurts. Leaving a misleading name beside a fix passes confusion to the next engineer.
Small, verified cleanup compounds over time.
8. Concurrency
8.1 Limit shared mutable state. Give concurrent work an explicit owner.
Uncontrolled mutation creates races that are hard to reproduce.
Better example
actor Counter { private(set) var value = 0 func increment() { value += 1 } }Why it helps. The update is coordinated so concurrent work cannot silently lose increments.
Costly example
final class Counter { var value = 0 func increment() { value += 1 } }Why it hurts. An unsynchronized read-modify-write can lose updates when workers race.
Give concurrent mutation an explicit owner or synchronization boundary.
8.2 Recheck after suspension. An await can let state change before execution resumes.
A multi-step async operation is not automatically atomic when other work can run between steps.
Better example
let userID = currentUser.id let data = try await api.load(userID) guard currentUser.id == userID else { return } profile = dataWhy it helps. The result is applied only if the selected user still matches the request that produced it.
Costly example
let user = currentUser let data = try await api.load(user.id) profile = dataWhy it hurts. A response for an old user can overwrite the profile after the selection changes during await.
Protect against stale async results.
8.3 Design cancellation and shutdown. Define what happens when concurrent work ends early.
Leaked tasks and stale callbacks can outlive the feature that started them.
Better example
func search(_ query: String) { searchTask?.cancel() searchTask = Task { let response = try await api.search(query) try Task.checkCancellation() results = response } }Why it helps. A new search cancels prior work and checks cancellation before applying the response.
Costly example
func search(_ query: String) { Task { results = try await api.search(query) } }Why it hurts. Each search starts a task that can outlive the latest query and overwrite newer results.
Test rapid retries, cancellation, and lifecycle edges.
8.4 Keep coordination separate. Test business rules without threads; isolate scheduling around them.
Mixing synchronization and domain logic makes both harder to reason about.
Better example
struct CartPricing { func total(for items: [Item]) -> Decimal { items.reduce(0) { $0 + $1.price } } } actor CartStore { /* coordinates items */ }Why it helps. Pure pricing logic stays testable without scheduling, while CartStore owns concurrent access.
Costly example
actor Cart { func total() async -> Decimal { // Fetch policy while summing every line } }Why it hurts. The coordinator mixes asynchronous fetching with arithmetic, making the rule harder to isolate.
Concurrency is a boundary around behavior, not the behavior itself.
8.5 Test competing execution paths. Exercise stale results and overlapping requests, not only single calls.
Concurrency bugs often require an unfavorable order of events.
Better example
func testOldResponseCannotReplaceNewQuery() async { await api.complete(newQuery, with: newResult) await api.complete(oldQuery, with: oldResult) XCTAssertEqual(viewModel.result, newResult) }Why it helps. The test forces old and new responses to complete out of order and checks the visible result.
Costly example
func testLoad() async { await viewModel.load() XCTAssertNotNil(viewModel.result) }Why it hurts. A single load test cannot expose stale-response races between overlapping requests.
Control scheduling in tests so a race is repeatable.