Friday, January 16, 2026

Clean Code Chapter 5: Formatting

 Clean Code Chapter 5: Formatting

Code formatting might seem like a trivial concern compared to architecture or algorithms, but Robert C. Martin dedicates an entire chapter to it in *Clean Code* for good reason. As he puts it: "Code formatting is about communication, and communication is the professional developer's first order of business."

When you format code, you're not just making it pretty. You're making it readable, and readable code is maintainable code. The choices you make about whitespace, line length, and organization affect how quickly another developer (or future you) can understand what's happening.

The Newspaper Metaphor

Martin introduces a powerful metaphor: think of your source file like a newspaper article. The headline tells you what the story is about and helps you decide whether to read further. The first paragraph gives you a synopsis. As you continue down, you get increasing levels of detail.

Your code should work the same way. The name of the file or class should tell readers what they're looking at. The topmost functions should provide high-level concepts. As you scroll down, details should emerge.

Vertical Formatting

Vertical formatting answers the question: how long should a source file be?

Martin's research on successful open source projects found that most files were under 500 lines, with an upper limit around 200 being typical. Smaller files are easier to understand. This doesn't mean you artificially break up cohesive code, but it does mean you should question any file that grows beyond a few hundred lines.

Vertical Openness Between Concepts

Blank lines serve as visual separators between distinct thoughts. Each blank line is a visual cue that a new concept is beginning.

The Problem:

```ruby
# Ruby - Dense, hard to scan
class InvoiceGenerator
attr_reader :order, :customer
def initialize(order)
@order = order
@customer = order.customer
end
def generate
validate_order
invoice = build_invoice
calculate_totals(invoice)
apply_discounts(invoice)
invoice.save!
send_notification(invoice)
invoice
end
private
def validate_order
raise InvalidOrderError unless order.finalized?
raise MissingCustomerError unless customer.present?
end
def build_invoice
Invoice.new(
order: order,
customer: customer,
issued_at: Time.current
)
end
def calculate_totals(invoice)
invoice.subtotal = order.line_items.sum(&:total)
invoice.tax = TaxCalculator.calculate(invoice.subtotal, customer.tax_region)
invoice.total = invoice.subtotal + invoice.tax
end
end
```

The Solution:

```ruby
# Ruby - Breathing room between concepts
class InvoiceGenerator
attr_reader :order, :customer

def initialize(order)
@order = order
@customer = order.customer
end

def generate
validate_order

invoice = build_invoice
calculate_totals(invoice)
apply_discounts(invoice)

invoice.save!
send_notification(invoice)

invoice
end

private

def validate_order
raise InvalidOrderError unless order.finalized?
raise MissingCustomerError unless customer.present?
end

def build_invoice
Invoice.new(
order: order,
customer: customer,
issued_at: Time.current
)
end

def calculate_totals(invoice)
invoice.subtotal = order.line_items.sum(&:total)
invoice.tax = TaxCalculator.calculate(invoice.subtotal, customer.tax_region)
invoice.total = invoice.subtotal + invoice.tax
end
end
```

The second version uses blank lines to create visual paragraphs. You can scan it quickly and understand the structure without reading every line.

Vertical Density

The flip side of openness is density. Lines of code that are tightly related should appear vertically close to each other. When you separate related concepts with blank lines, you force readers to hunt for connections.

```ruby
# Ruby - Related things should stay together
class ReportConfig
attr_reader :title,
:date_range,
:include_charts

def initialize(title:, date_range:, include_charts: true)
@title = title
@date_range = date_range
@include_charts = include_charts
end
end
```

Notice how the attribute declarations stay together without blank lines between them. They're all part of the same concept: "what data this class holds."

Vertical Distance

Related concepts should not only be close together, they should also be ordered in a way that helps readers. Martin outlines several principles.

Variable declarations should appear as close to their usage as possible. In short functions, that usually means at the top.

Instance variables should be declared at the top of the class in a consistent location. Ruby's convention of `attr_reader` declarations at the top serves this well.

Dependent functions should be vertically close. If one function calls another, the caller should be above the callee where possible, and they should be nearby. This creates a natural downward flow through the source code.

```ruby
# Ruby - Caller above callee, close together
class OrderFulfillment
def fulfill(order)
return unless fulfillable?(order)

reserve_inventory(order)
schedule_shipment(order)
notify_customer(order)
end

private

def fulfillable?(order)
order.paid? && inventory_available?(order)
end

def inventory_available?(order)
order.line_items.all? do |item|
Inventory.available?(item.sku, item.quantity)
end
end

def reserve_inventory(order)
order.line_items.each do |item|
Inventory.reserve(item.sku, item.quantity)
end
end

def schedule_shipment(order)
ShippingService.schedule(order)
end

def notify_customer(order)
OrderMailer.fulfillment_started(order).deliver_later
end
end
```

The public `fulfill` method sits at the top. The private methods it calls are ordered roughly in the sequence they're invoked. A reader can follow the flow without jumping around.

Horizontal Formatting

How long should a line be? Martin argues for keeping lines short enough that you never need to scroll horizontally. His suggestion: aim for 120 characters or fewer.

Modern monitors are wide, but that doesn't mean you should fill them. Shorter lines are easier to scan, and they work better with side-by-side diffs during code review.

Horizontal Openness and Density

Just as blank lines separate concepts vertically, spaces separate things horizontally. Use them to show association and disassociation.

```ruby
# Ruby - Spaces showing relationships
def calculate_score(base, multiplier, bonus)
adjusted = base*multiplier + bonus # Multiplication binds tighter
normalized = adjusted / MAX_SCORE
[normalized, 1.0].min
end
```

In practice, Ruby style guides (and most linters) prefer spaces around all operators for consistency. The point is that spacing conveys meaning. Assignment operators get space on both sides because they separate two distinct things. Method arguments don't get space after the method name because they're closely associated.

Indentation

Indentation makes the hierarchy of scopes visible. Without it, code becomes nearly impossible to read.

The Problem:

```jsx
// React - Collapsed indentation destroys readability
function UserDashboard({user}) {
return (<div className="dashboard">
<header><h1>Welcome, {user.name}</h1>
<nav>{user.isAdmin && <AdminLink />}</nav></header>
<main><UserStats stats={user.stats} />
<RecentActivity activities={user.recentActivities} /></main>
</div>);}
```

The Solution:

```jsx
// React - Proper indentation reveals structure
function UserDashboard({ user }) {
return (
<div className="dashboard">
<header>
<h1>Welcome, {user.name}</h1>
<nav>
{user.isAdmin && <AdminLink />}
</nav>
</header>

<main>
<UserStats stats={user.stats} />
<RecentActivity activities={user.recentActivities} />
</main>
</div>
);
}
```

The indentation immediately shows you the DOM hierarchy. You can scan the left edge and understand the structure without reading the content.

Breaking Indentation

Sometimes developers are tempted to collapse short statements onto single lines. Resist this urge.

```ruby
# Ruby - Don't collapse control structures
class User
# Tempting but harder to scan
def active?; status == 'active'; end
def admin?; role == 'admin'; end

# Better: consistent structure
def active?
status == 'active'
end

def admin?
role == 'admin'
end
end
```

The multi-line version takes more vertical space, but it maintains visual consistency. Your eye can scan down the left margin and know exactly what it will find.

Team Rules

Martin emphasizes that a team of developers should agree on a single formatting style. Individual preference matters less than consistency across the codebase.

In Ruby, this is largely solved by tools like RuboCop. In JavaScript and TypeScript, Prettier handles it automatically. The specific rules matter less than having rules everyone follows.

```ruby
# .rubocop.yml - Enforce team consistency
Layout/LineLength:
Max: 120

Layout/EmptyLinesAroundClassBody:
EnforcedStyle: empty_lines

Layout/MultilineMethodCallIndentation:
EnforcedStyle: indented
```

```javascript
// .prettierrc - Remove formatting debates
{
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "es5"
}
```

Once you've configured these tools, formatting debates end. The computer enforces consistency, and developers can focus on what the code does rather than how it looks.

Putting It Together: A React Example

Let's look at a complete React component demonstrating these principles.

The Problem:

```jsx
// React - Poor formatting obscures intent
import {useState,useEffect,useCallback} from 'react';
import {fetchUserData,updateUserPreferences} from '../api/users';
export function UserPreferences({userId,onSave}) {
const [preferences,setPreferences] = useState(null);
const [loading,setLoading] = useState(true);
const [error,setError] = useState(null);
useEffect(() => {fetchUserData(userId).then(data => {setPreferences(data.preferences);setLoading(false);}).catch(err => {setError(err.message);setLoading(false);});},[userId]);
const handleToggle = useCallback((key) => {setPreferences(prev => ({...prev,[key]: !prev[key]}));},[]);
const handleSave = useCallback(async () => {try {await updateUserPreferences(userId,preferences);onSave(preferences);} catch (err) {setError(err.message);}},[userId,preferences,onSave]);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
return (<div className="preferences"><h2>Your Preferences</h2><PreferenceToggle label="Email notifications" checked={preferences.emailNotifications} onChange={() => handleToggle('emailNotifications')} /><PreferenceToggle label="Dark mode" checked={preferences.darkMode} onChange={() => handleToggle('darkMode')} /><button onClick={handleSave}>Save Changes</button></div>);}
```

The Solution:

```jsx
// React - Clean formatting aids comprehension
import { useState, useEffect, useCallback } from 'react';

import { fetchUserData, updateUserPreferences } from '../api/users';

export function UserPreferences({ userId, onSave }) {
const [preferences, setPreferences] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
fetchUserData(userId)
.then(data => {
setPreferences(data.preferences);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]);

const handleToggle = useCallback((key) => {
setPreferences(prev => ({
...prev,
[key]: !prev[key]
}));
}, []);

const handleSave = useCallback(async () => {
try {
await updateUserPreferences(userId, preferences);
onSave(preferences);
} catch (err) {
setError(err.message);
}
}, [userId, preferences, onSave]);

if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;

return (
<div className="preferences">
<h2>Your Preferences</h2>

<PreferenceToggle
label="Email notifications"
checked={preferences.emailNotifications}
onChange={() => handleToggle('emailNotifications')}
/>

<PreferenceToggle
label="Dark mode"
checked={preferences.darkMode}
onChange={() => handleToggle('darkMode')}
/>

<button onClick={handleSave}>Save Changes</button>
</div>
);
}
```

The formatted version:

1. Groups imports with blank lines separating external and internal modules
2. Keeps related state declarations together
3. Separates logical sections (state, effects, handlers, render) with blank lines
4. Breaks JSX across lines so the structure is visible
5. Uses consistent indentation throughout

The Bottom Line

Formatting is about respect. Respect for the readers of your code, for your teammates, and for your future self. Martin writes: "Perhaps you thought that 'getting it working' was the first order of business for a professional developer. I hope by now this book has disabused you of that idea."

Code is read far more often than it is written. Every formatting choice either helps or hinders that reading. Choose wisely, automate what you can, and remember that the goal is communication.

---

What formatting conventions does your team follow? What tools have helped standardize your codebase? I'd love to hear what's worked for you.

Wednesday, January 7, 2026

Clean Code Chapter 4: Comments, or The Art of Saying Nothing

 

Clean Code Chapter 4: Comments, or The Art of Saying Nothing

One of the most counterintuitive lessons in Robert Martin's Clean Code appears in Chapter 4: the best comment is often no comment at all. As Martin puts it, "the proper use of comments is to compensate for our failure to express ourselves in code."

This doesn't mean comments are evil. It means they're a last resort. When you feel the urge to write a comment, your first instinct should be to ask: Can I refactor this code so the comment becomes unnecessary?

Why Comments Lie

Comments have a fundamental problem: they rot. Code changes, deadlines loom, and comments get left behind. Unlike code, comments don't throw errors when they become inaccurate. A misleading comment is worse than no comment at all because it actively deceives the reader.

Martin argues that we should spend our energy making code self-documenting rather than explaining unclear code with comments.

Good Comments vs. Bad Comments

Not all comments are bad. Martin identifies several legitimate uses:

Acceptable comments:

  • Legal notices (copyright, licensing)
  • Explanation of intent when the why isn't obvious from code
  • Clarification of obscure arguments or return values from external libraries
  • Warnings of consequences
  • TODO markers (used sparingly)

Comments to avoid:

  • Redundant comments that repeat what the code says
  • Commented-out code (that's what version control is for)
  • Position markers and banners
  • Attributions (again, version control)
  • Closing brace comments

Rails Example: Let the Code Speak

Here's a common pattern I see in Rails applications with over-commenting:

# Bad: Comments that add noise
class OrderProcessor
  # Process the order
  def process(order)
    # Check if order is valid
    return unless order.valid?

    # Calculate the total price of all items
    total = order.line_items.sum do |item|
      # Multiply quantity by unit price
      item.quantity * item.unit_price
    end

    # Apply discount if customer has one
    if order.customer.discount_percentage.present?
      # Calculate discount amount
      discount = total * (order.customer.discount_percentage / 100.0)
      # Subtract discount from total
      total = total - discount
    end

    # Save the total to the order
    order.update(total_amount: total)

    # Send confirmation email to customer
    OrderMailer.confirmation(order).deliver_later
  end
end

Every comment here restates what the code already says. The method name process is vague, forcing us to read comments to understand what's happening. Let's refactor:

# Good: Self-documenting code
class OrderProcessor
  def finalize_and_confirm(order)
    return unless order.valid?

    order.update(total_amount: calculate_discounted_total(order))
    OrderMailer.confirmation(order).deliver_later
  end

  private

  def calculate_discounted_total(order)
    subtotal = order.line_items.sum(&:total_price)
    apply_customer_discount(subtotal, order.customer)
  end

  def apply_customer_discount(amount, customer)
    return amount unless customer.discount_percentage.present?

    amount * (1 - customer.discount_percentage / 100.0)
  end
end

The refactored version uses method names that explain intent. finalize_and_confirm tells you exactly what the public method does. calculate_discounted_total and apply_customer_discount make the business logic readable without a single comment.

Notice how extracting total_price to the LineItem model (implied by sum(&:total_price)) moves that logic where it belongs. The code reads like a sentence describing the business process.

React Example: When Comments Help and When They Hurt

Frontend code often accumulates comments explaining complex UI logic. Here's a React component drowning in unnecessary comments:

// Bad: Comment noise obscuring the code
interface CartProps {
  items: CartItem[];
  onCheckout: () => void;
}

function ShoppingCart({ items, onCheckout }: CartProps) {
  // State for showing the discount input
  const [showDiscount, setShowDiscount] = useState(false);
  // State for the discount code value
  const [discountCode, setDiscountCode] = useState('');
  // State for discount validation error
  const [error, setError] = useState<string | null>(null);

  // Calculate the total price
  const total = items.reduce((sum, item) => {
    // Add item price times quantity to sum
    return sum + item.price * item.quantity;
  }, 0); // Start with 0

  // Handle applying the discount code
  const handleApplyDiscount = () => {
    // Check if code is valid
    if (discountCode.length < 5) {
      // Set error message
      setError('Invalid discount code');
      return;
    }
    // Clear error and apply
    setError(null);
    // TODO: Actually apply discount
  };

  // Render the component
  return (
    <div className="cart">
      {/* Map through items and display them */}
      {items.map((item) => (
        // Use item id as key
        <CartItemRow key={item.id} item={item} />
      ))}
      {/* Show total */}
      <div className="total">Total: ${total.toFixed(2)}</div>
      {/* Checkout button */}
      <button onClick={onCheckout}>Checkout</button>
    </div>
  );
}

The comments here are pure noise. // Calculate the total price before a variable called total adds nothing. // Render the component before a return statement is almost comical. Let's clean this up:

// Good: Clear code with one meaningful comment
interface CartProps {
  items: CartItem[];
  onCheckout: () => void;
}

function ShoppingCart({ items, onCheckout }: CartProps) {
  const [discountForm, setDiscountForm] = useState({
    isVisible: false,
    code: '',
    error: null as string | null,
  });

  const cartTotal = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  const applyDiscountCode = () => {
    const MIN_CODE_LENGTH = 5;

    if (discountForm.code.length < MIN_CODE_LENGTH) {
      setDiscountForm((prev) => ({ ...prev, error: 'Invalid discount code' }));
      return;
    }

    setDiscountForm((prev) => ({ ...prev, error: null }));
    // TODO: Integrate with discount API once pricing service is deployed
  };

  return (
    <div className="cart">
      {items.map((item) => (
        <CartItemRow key={item.id} item={item} />
      ))}

      <div className="total">Total: ${cartTotal.toFixed(2)}</div>
      <button onClick={onCheckout}>Checkout</button>
    </div>
  );
}

The refactored version groups related state into a single object with descriptive property names. cartTotal is more specific than total. The magic number 5 becomes a named constant MIN_CODE_LENGTH.

One comment remains: the TODO. This is a legitimate use because it explains why the code is incomplete (waiting on an external dependency) rather than restating what the code does.

The Litmus Test

Before writing a comment, ask yourself these questions:

  1. Can I rename this variable or function to eliminate the need for this comment?
  2. Can I extract this logic into a well-named method?
  3. Am I explaining what the code does (bad) or why it does something non-obvious (potentially acceptable)?
  4. Will this comment stay accurate as the code evolves?

If you can refactor instead of comment, do it. Your future self (and your teammates) will thank you.

The Takeaway

Comments aren't inherently bad, but they should make you pause. Each comment is an admission that your code couldn't speak for itself. Sometimes that's unavoidable. More often, it's an invitation to write clearer code.

As Martin writes: "The only truly good comment is the comment you found a way not to write."

Tuesday, December 30, 2025

Clean Code Chapter 3: Functions

 

Clean Code Chapter 3: Functions

Writing Functions That Tell a Story

In Chapter 3 of Clean Code, Robert C. Martin lays out a compelling vision: functions should be small, do one thing, and read like well-written prose. This chapter transformed how I think about structuring code, and the principles apply beautifully to both Rails backends and React frontends.

Let's explore the key ideas with practical examples.


Small!

Martin's first rule of functions is that they should be small. His second rule? They should be smaller than that.

Functions should rarely be more than 20 lines. Ideally, they should be 5-10 lines. This isn't arbitrary. Small functions are easier to understand, test, and maintain.

Rails Example

Before: A bloated controller action

def create
  @user = User.new(user_params)
  
  if @user.save
    if params[:subscribe_to_newsletter]
      newsletter = Newsletter.find_by(name: 'weekly')
      if newsletter
        Subscription.create(user: @user, newsletter: newsletter)
        NewsletterMailer.welcome_email(@user).deliver_later
      end
    end
    
    if @user.referral_code.present?
      referrer = User.find_by(referral_code: @user.referral_code)
      if referrer
        Credit.create(user: referrer, amount: 10, reason: 'referral')
        Credit.create(user: @user, amount: 5, reason: 'referred')
        ReferralMailer.successful_referral(referrer, @user).deliver_later
      end
    end
    
    redirect_to dashboard_path, notice: 'Welcome!'
  else
    render :new
  end
end

After: Small, focused functions

def create
  @user = User.new(user_params)
  
  if @user.save
    handle_newsletter_subscription
    process_referral
    redirect_to dashboard_path, notice: 'Welcome!'
  else
    render :new
  end
end

private

def handle_newsletter_subscription
  return unless params[:subscribe_to_newsletter]
  
  NewsletterSubscriptionService.subscribe(@user, 'weekly')
end

def process_referral
  return unless @user.referral_code.present?
  
  ReferralService.process(@user, @user.referral_code)
end

React Example

Before: A component doing too much

function UserDashboard({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [orders, setOrders] = useState<Order[]>([]);
  const [notifications, setNotifications] = useState<Notification[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        return fetch(`/api/users/${userId}/orders`);
      })
      .then(res => res.json())
      .then(data => {
        setOrders(data);
        return fetch(`/api/users/${userId}/notifications`);
      })
      .then(res => res.json())
      .then(data => {
        setNotifications(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <Spinner />;

  return (
    <div>
      <h1>Welcome, {user?.name}</h1>
      <div className="stats">
        <div>Orders: {orders.length}</div>
        <div>Unread: {notifications.filter(n => !n.read).length}</div>
      </div>
      <ul>
        {orders.map(order => (
          <li key={order.id}>
            Order #{order.id} - ${order.total} - {order.status}
          </li>
        ))}
      </ul>
    </div>
  );
}

After: Composed from smaller pieces

function UserDashboard({ userId }: { userId: string }) {
  const { user, orders, notifications, loading } = useDashboardData(userId);

  if (loading) return <Spinner />;

  return (
    <div>
      <WelcomeHeader userName={user?.name} />
      <DashboardStats orderCount={orders.length} notifications={notifications} />
      <OrderList orders={orders} />
    </div>
  );
}

function DashboardStats({ 
  orderCount, 
  notifications 
}: { 
  orderCount: number; 
  notifications: Notification[];
}) {
  const unreadCount = notifications.filter(n => !n.read).length;
  
  return (
    <div className="stats">
      <div>Orders: {orderCount}</div>
      <div>Unread: {unreadCount}</div>
    </div>
  );
}

Do One Thing

Functions should do one thing. They should do it well. They should do it only.

But how do you know if a function does "one thing"? Martin offers a useful test: if you can extract another function from it with a name that isn't merely a restatement of its implementation, it's doing more than one thing.

Rails Example

Before: A method doing multiple things

def process_order(order)
  # Validate inventory
  order.line_items.each do |item|
    product = item.product
    if product.inventory_count < item.quantity
      raise InsufficientInventoryError, "Not enough #{product.name}"
    end
  end
  
  # Calculate totals
  subtotal = order.line_items.sum { |item| item.price * item.quantity }
  tax = subtotal * order.tax_rate
  shipping = calculate_shipping(order)
  total = subtotal + tax + shipping
  
  # Update order
  order.update!(
    subtotal: subtotal,
    tax: tax,
    shipping: shipping,
    total: total,
    status: 'confirmed'
  )
  
  # Send notifications
  OrderMailer.confirmation(order).deliver_later
  InventoryService.reserve_items(order)
end

After: Each function does one thing

def process_order(order)
  validate_inventory(order)
  finalize_totals(order)
  confirm_order(order)
  send_notifications(order)
end

private

def validate_inventory(order)
  InventoryValidator.validate!(order)
end

def finalize_totals(order)
  OrderTotalCalculator.calculate!(order)
end

def confirm_order(order)
  order.update!(status: 'confirmed')
end

def send_notifications(order)
  OrderNotifier.send_confirmation(order)
  InventoryService.reserve_items(order)
end

React Example

Before: A handler doing multiple things

function handleSubmit(event: FormEvent) {
  event.preventDefault();
  
  // Validation
  const errors: string[] = [];
  if (!formData.email.includes('@')) {
    errors.push('Invalid email');
  }
  if (formData.password.length < 8) {
    errors.push('Password too short');
  }
  if (formData.password !== formData.confirmPassword) {
    errors.push('Passwords do not match');
  }
  
  if (errors.length > 0) {
    setErrors(errors);
    return;
  }
  
  // Transform data
  const payload = {
    email: formData.email.toLowerCase().trim(),
    password: formData.password,
    marketingOptIn: formData.newsletter,
  };
  
  // Submit
  setSubmitting(true);
  fetch('/api/register', {
    method: 'POST',
    body: JSON.stringify(payload),
  })
    .then(res => res.json())
    .then(data => {
      localStorage.setItem('token', data.token);
      navigate('/dashboard');
    })
    .catch(err => setErrors([err.message]))
    .finally(() => setSubmitting(false));
}

After: Separated concerns

function handleSubmit(event: FormEvent) {
  event.preventDefault();
  
  const validationErrors = validateRegistrationForm(formData);
  if (validationErrors.length > 0) {
    setErrors(validationErrors);
    return;
  }
  
  submitRegistration(formData);
}

function validateRegistrationForm(data: RegistrationFormData): string[] {
  const errors: string[] = [];
  
  if (!isValidEmail(data.email)) errors.push('Invalid email');
  if (!isValidPassword(data.password)) errors.push('Password too short');
  if (!passwordsMatch(data.password, data.confirmPassword)) {
    errors.push('Passwords do not match');
  }
  
  return errors;
}

async function submitRegistration(data: RegistrationFormData) {
  setSubmitting(true);
  
  try {
    const payload = buildRegistrationPayload(data);
    const response = await registerUser(payload);
    handleSuccessfulRegistration(response);
  } catch (err) {
    setErrors([err.message]);
  } finally {
    setSubmitting(false);
  }
}

One Level of Abstraction per Function

Functions should maintain a consistent level of abstraction. Mixing high-level concepts with low-level details creates cognitive dissonance.

Rails Example

Before: Mixed abstraction levels

def onboard_new_customer(customer_params)
  # High level: create customer
  customer = Customer.create!(customer_params)
  
  # Low level: SQL for finding default plan
  default_plan = Plan.where(active: true)
                     .where('price_cents > 0')
                     .order(:price_cents)
                     .first
  
  # High level: create subscription
  subscription = customer.subscriptions.create!(plan: default_plan)
  
  # Low level: date calculation
  trial_end = Time.current + 14.days
  trial_end = trial_end.end_of_day
  trial_end = trial_end.in_time_zone(customer.timezone)
  
  subscription.update!(trial_ends_at: trial_end)
  
  # High level: send welcome
  CustomerMailer.welcome(customer).deliver_later
end

After: Consistent abstraction

def onboard_new_customer(customer_params)
  customer = create_customer(customer_params)
  start_trial_subscription(customer)
  send_welcome_email(customer)
  customer
end

private

def create_customer(params)
  Customer.create!(params)
end

def start_trial_subscription(customer)
  plan = Plan.default_starter_plan
  trial_end = TrialPeriod.calculate_end_date(customer.timezone)
  
  customer.subscriptions.create!(
    plan: plan,
    trial_ends_at: trial_end
  )
end

def send_welcome_email(customer)
  CustomerMailer.welcome(customer).deliver_later
end

React Example

Before: Mixed abstractions in a component

function CheckoutPage() {
  const cart = useCart();
  
  // High level
  const handleCheckout = async () => {
    // Low level: manual localStorage manipulation
    const savedAddress = localStorage.getItem('shipping_address');
    const address = savedAddress ? JSON.parse(savedAddress) : null;
    
    // Low level: manual API construction
    const response = await fetch('/api/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
      },
      body: JSON.stringify({
        items: cart.items.map(i => ({ id: i.productId, qty: i.quantity })),
        shipping: address,
      }),
    });
    
    // High level
    if (response.ok) {
      cart.clear();
      navigate('/confirmation');
    }
  };
  
  return <CheckoutForm onSubmit={handleCheckout} />;
}

After: Consistent high-level abstraction

function CheckoutPage() {
  const cart = useCart();
  const { createOrder } = useOrders();
  const { shippingAddress } = useShippingAddress();
  
  const handleCheckout = async () => {
    const order = await createOrder({
      items: cart.items,
      shippingAddress,
    });
    
    if (order) {
      cart.clear();
      navigate('/confirmation');
    }
  };
  
  return <CheckoutForm onSubmit={handleCheckout} />;
}

Use Descriptive Names

Don't be afraid to make a name long. A long descriptive name is better than a short enigmatic name.

The name should describe what the function does. If you struggle to name it, that's a sign it might be doing too much.

Rails Example

Before: Vague names

def process(user)
  # What does this process?
end

def handle_data(params)
  # Handle how?
end

def do_stuff(order)
  # Very helpful...
end

def check(item)
  # Check what exactly?
end

After: Names that explain intent

def send_password_reset_email(user)
end

def normalize_address_params(params)
end

def apply_promotional_discount(order)
end

def inventory_sufficient_for?(item)
end

React Example

Before: Unclear names

function handle(e) { }
function update(data) { }
function process(items) { }
function getData() { }
function Component1() { }

After: Self-documenting names

function handleEmailChange(event: ChangeEvent<HTMLInputElement>) { }
function updateUserProfile(profileData: ProfileUpdate) { }
function calculateCartSubtotal(lineItems: CartItem[]) { }
function fetchActiveSubscriptions() { }
function SubscriptionPlanSelector() { }

Function Arguments

The ideal number of arguments is zero. Next comes one, then two. Three arguments should be avoided where possible. More than three requires special justification.

Rails Example

Before: Too many arguments

def create_shipment(order_id, carrier, service_level, weight, dimensions, 
                    signature_required, insurance_amount, saturday_delivery)
  # ...
end

# Calling code
create_shipment(
  order.id, 'fedex', 'overnight', 2.5, [10, 8, 4],
  true, 500.00, false
)

After: Using an argument object

def create_shipment(shipment_request)
  carrier = shipment_request.carrier
  # ...
end

# With a value object or struct
shipment_request = ShipmentRequest.new(
  order: order,
  carrier: 'fedex',
  service_level: 'overnight',
  package: Package.new(weight: 2.5, dimensions: [10, 8, 4]),
  options: ShippingOptions.new(
    signature_required: true,
    insurance_amount: 500.00,
    saturday_delivery: false
  )
)

create_shipment(shipment_request)

React Example

Before: Props explosion

function ProductCard({
  name,
  price,
  originalPrice,
  imageUrl,
  rating,
  reviewCount,
  inStock,
  onAddToCart,
  onAddToWishlist,
  onQuickView,
  showRating,
  showWishlist,
  variant,
}: ProductCardProps) {
  // 13 props!
}

After: Grouped into logical objects

interface ProductCardProps {
  product: Product;
  actions: ProductActions;
  display?: DisplayOptions;
}

function ProductCard({ product, actions, display = defaultDisplay }: ProductCardProps) {
  const { name, price, imageUrl } = product;
  const { onAddToCart, onAddToWishlist } = actions;
  const { showRating, variant } = display;
  
  // Much cleaner
}

// Usage
<ProductCard 
  product={product}
  actions={{ onAddToCart, onAddToWishlist, onQuickView }}
  display={{ showRating: true, variant: 'compact' }}
/>

Have No Side Effects

Side effects are lies. Your function promises to do one thing, but it also does other hidden things.

Rails Example

Before: Hidden side effect

def authenticate(username, password)
  user = User.find_by(username: username)
  return false unless user
  
  if user.valid_password?(password)
    # Hidden side effect! The method name suggests it only authenticates
    session[:user_id] = user.id
    user.update!(last_login_at: Time.current)
    true
  else
    false
  end
end

After: Explicit about what it does

def authenticate(username, password)
  user = User.find_by(username: username)
  return nil unless user&.valid_password?(password)
  
  user
end

def sign_in(user)
  session[:user_id] = user.id
  record_login(user)
end

def record_login(user)
  user.update!(last_login_at: Time.current)
end

# Usage is now explicit
if user = authenticate(username, password)
  sign_in(user)
  redirect_to dashboard_path
end

React Example

Before: Sneaky side effects

function formatCurrency(amount: number): string {
  // Side effect: logging
  console.log(`Formatting: ${amount}`);
  
  // Side effect: analytics
  analytics.track('currency_formatted', { amount });
  
  // Side effect: caching in global state
  window.__lastFormattedAmount = amount;
  
  return `$${amount.toFixed(2)}`;
}

After: Pure function

function formatCurrency(amount: number): string {
  return `$${amount.toFixed(2)}`;
}

// If you need tracking, make it explicit
function formatAndTrackCurrency(amount: number): string {
  analytics.track('currency_formatted', { amount });
  return formatCurrency(amount);
}

Command Query Separation

Functions should either do something (command) or answer something (query), but not both.

Rails Example

Before: Mixed command and query

def set_and_return_status(order, new_status)
  order.update!(status: new_status)
  order.status  # Returns something
end

# Confusing usage
if set_and_return_status(order, 'shipped') == 'shipped'
  # Did it change? Was it already shipped?
end

After: Separate command and query

# Command: changes state, returns nothing meaningful
def update_status(order, new_status)
  order.update!(status: new_status)
end

# Query: returns information, changes nothing
def current_status(order)
  order.status
end

# Clear usage
update_status(order, 'shipped')
if current_status(order) == 'shipped'
  notify_customer(order)
end

React Example

Before: Mutation returns value

function toggleAndGetVisibility(): boolean {
  setIsVisible(prev => !prev);
  return !isVisible; // This might not be what you expect due to async state
}

After: Separated

// Command
function toggleVisibility(): void {
  setIsVisible(prev => !prev);
}

// Query (derived state)
const isCurrentlyVisible = isVisible;

// Or use a callback pattern if you need the new value
function toggleVisibility(onToggled?: (newValue: boolean) => void): void {
  setIsVisible(prev => {
    const newValue = !prev;
    onToggled?.(newValue);
    return newValue;
  });
}

Prefer Exceptions to Returning Error Codes

Returning error codes means the caller must deal with the error immediately. Exceptions let you separate the happy path from error handling.

Rails Example

Before: Error codes

def create_account(params)
  return :invalid_email unless valid_email?(params[:email])
  return :email_taken if User.exists?(email: params[:email])
  return :weak_password unless strong_password?(params[:password])
  
  user = User.create!(params)
  :success
end

# Calling code becomes a mess
result = create_account(params)
case result
when :success then redirect_to dashboard_path
when :invalid_email then flash[:error] = "Invalid email"
when :email_taken then flash[:error] = "Email already registered"
when :weak_password then flash[:error] = "Password too weak"
end

After: Exceptions

def create_account(params)
  validate_email!(params[:email])
  ensure_email_available!(params[:email])
  validate_password_strength!(params[:password])
  
  User.create!(params)
end

# Clean calling code
begin
  create_account(params)
  redirect_to dashboard_path
rescue InvalidEmailError => e
  flash[:error] = e.message
  render :new
rescue EmailTakenError => e
  flash[:error] = "Email already registered"
  render :new
rescue WeakPasswordError => e
  flash[:error] = e.message
  render :new
end

React/TypeScript Example

Before: Error returns

type Result = { success: true; data: User } | { success: false; error: string };

async function fetchUser(id: string): Promise<Result> {
  try {
    const response = await api.get(`/users/${id}`);
    return { success: true, data: response.data };
  } catch {
    return { success: false, error: 'Failed to fetch user' };
  }
}

// Awkward usage
const result = await fetchUser(id);
if (result.success) {
  setUser(result.data);
} else {
  setError(result.error);
}

After: Let exceptions flow (with proper boundaries)

async function fetchUser(id: string): Promise<User> {
  const response = await api.get(`/users/${id}`);
  return response.data;
}

// With an error boundary or try/catch at the appropriate level
function UserProfile({ userId }: { userId: string }) {
  const { data: user, error, isLoading } = useQuery(
    ['user', userId],
    () => fetchUser(userId)
  );
  
  if (error) return <ErrorDisplay error={error} />;
  if (isLoading) return <Spinner />;
  
  return <ProfileCard user={user} />;
}

Don't Repeat Yourself (DRY)

Duplication is the root of all evil in software. Every piece of knowledge should have a single, unambiguous representation in the system.

Rails Example

Before: Duplicated logic

class OrdersController < ApplicationController
  def create
    @order = Order.new(order_params)
    
    # Calculate total
    subtotal = @order.line_items.sum { |i| i.price * i.quantity }
    discount = @order.coupon&.calculate_discount(subtotal) || 0
    tax = (subtotal - discount) * 0.08
    @order.total = subtotal - discount + tax
    
    @order.save!
  end
end

class Order < ApplicationRecord
  def recalculate_total
    # Same logic repeated!
    subtotal = line_items.sum { |i| i.price * i.quantity }
    discount = coupon&.calculate_discount(subtotal) || 0
    tax = (subtotal - discount) * 0.08
    self.total = subtotal - discount + tax
    save!
  end
end

After: Single source of truth

class OrderTotalCalculator
  def initialize(order)
    @order = order
  end
  
  def calculate
    subtotal - discount + tax
  end
  
  def subtotal
    @order.line_items.sum { |i| i.price * i.quantity }
  end
  
  def discount
    @order.coupon&.calculate_discount(subtotal) || 0
  end
  
  def tax
    (subtotal - discount) * TAX_RATE
  end
  
  private
  
  TAX_RATE = 0.08
end

# Used everywhere
@order.total = OrderTotalCalculator.new(@order).calculate

React Example

Before: Copy-pasted validation

function RegistrationForm() {
  const validateEmail = (email: string) => {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
  };
  // ...
}

function ProfileForm() {
  const validateEmail = (email: string) => {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
  };
  // ...
}

function InviteForm() {
  const validateEmail = (email: string) => {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
  };
  // ...
}

After: Shared utility

// utils/validation.ts
export function isValidEmail(email: string): boolean {
  const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return EMAIL_REGEX.test(email);
}

// Or as a custom hook for form integration
export function useEmailValidation() {
  const validate = useCallback((email: string) => {
    if (!email) return 'Email is required';
    if (!isValidEmail(email)) return 'Invalid email format';
    return undefined;
  }, []);
  
  return { validate };
}

Conclusion

Martin's principles for writing clean functions are timeless. Whether you're building Rails APIs or React interfaces, the rules remain the same:

  1. Keep functions small and focused
  2. Make them do one thing well
  3. Maintain consistent abstraction levels
  4. Choose names that reveal intent
  5. Minimize arguments
  6. Avoid side effects
  7. Separate commands from queries
  8. Use exceptions over error codes
  9. Eliminate duplication

The goal isn't to follow these rules dogmatically, but to write code that communicates clearly. When your functions are small, well-named, and focused, your codebase becomes a joy to work in rather than a chore to maintain.


This is part of a series on Clean Code principles with Rails and React examples. See also: Chapter 1: Clean Code and Chapter 2: Meaningful Names.

Friday, December 26, 2025

The Five Dysfunctions of Software Teams, Part 1: Absence of Trust

The Five Dysfunctions of Software Teams, Part 1: Absence of Trust

This is the first post in a six-part series exploring The Five Dysfunctions of a Team through the lens of modern software engineering.

I recently read The Five Dysfunctions of a Team because a company I interviewed with said they base their culture on it. I was curious whether it would feel like generic leadership advice or something that actually mapped to real work.

It resonated with me more than I expected, especially as an individual contributor.

Not because the ideas were flashy or new, but because they described patterns I have seen repeatedly in engineering organizations that otherwise look healthy. Smart people. Reasonable processes. Solid intentions. And still, things quietly break down.

The first dysfunction, absence of trust, sits at the base of Lencioni’s model. If it is present, everything above it struggles. In software teams, it often shows up in subtle, normalized ways.

What lack of trust looks like in engineering

On software teams, lack of trust rarely looks like open hostility. It looks like:

  • Engineers avoiding areas of the codebase they do not “own”

  • Over-reliance on one or two people who “know how it works”

  • Pull requests that get approved without meaningful review

  • Bugs fixed in isolation instead of discussed in the open

  • Retrospectives that stay shallow and polite

None of this feels dramatic. That is why it is so dangerous.

How this breaks agile in practice

Agile assumes trust. Not as a nice-to-have, but as a prerequisite.

Consider what agile asks teams to do:

  • Share unfinished work early

  • Inspect and adapt frequently

  • Surface blockers quickly

  • Learn from failure without blame

Without trust, those practices turn into rituals instead of tools.

Daily standups become status reporting instead of problem solving.
Sprint reviews show only safe, polished work.
Retrospectives avoid the real issues because no one wants to look incompetent or difficult.

The process keeps running, but the learning stops.

The myth of the “strong IC”

As individual contributors, many of us learned that being good means being self-sufficient. You debug alone. You figure it out. You do not slow others down with questions.

That mindset feels professional, but it creates brittle teams.

When trust is low:

  • Knowledge stays siloed

  • Onboarding takes forever

  • Bus factor drops to one or two people

  • The team cannot respond well to change

Ironically, the more everyone tries to look strong individually, the weaker the team becomes.

Trust is built through small, visible behaviors

Trust on engineering teams is not built through offsites or trust exercises. It is built in the work.

From an IC perspective, that looks like:

  • Asking questions early, especially basic ones

  • Narrating your thinking in pull requests

  • Admitting when you broke something, quickly and publicly

  • Pairing or mobbing on risky changes

  • Writing down what you learn instead of keeping it in your head

These actions feel vulnerable, especially in cultures that reward speed and certainty. But they are exactly what allow agile practices to work as intended.

Blamelessness is not about being nice

Blameless postmortems are often misunderstood as being soft. They are not.

They are about shifting focus from “who messed up” to “how did the system allow this to happen.”

When teams trust each other:

  • Incidents become learning opportunities

  • Engineers speak honestly about near-misses

  • Fixes address root causes, not just symptoms

Without trust, postmortems become performative or avoided entirely. The same failures repeat under new names.

A practical takeaway for ICs

You do not need to be a manager to improve trust on your team.

Pick one behavior and practice it consistently:

  • Ask the question you think you should already know

  • Say “I do not understand this yet” in a design discussion

  • Call out uncertainty instead of masking it with confidence

Agile is not about moving fast. It is about learning fast.
Learning fast requires trust.
And trust starts with individual contributors being willing to be seen as human.


Up next: Part 2 will look at Fear of Conflict, and how avoiding disagreement in design reviews and architectural discussions quietly erodes software quality.

Series note:
This post is part of a six-part series applying The Five Dysfunctions of a Team to software engineering, with a focus on how these patterns show up in day-to-day work for individual contributors.

If this resonated, the next post explores Fear of Conflict and how avoiding disagreement in design reviews, pull requests, and architectural decisions quietly degrades code quality and team health.

You do not need to be a manager to influence any of this. You just need to notice the patterns and decide how you want to show up in them.

Thursday, December 25, 2025

The Art of Naming: Chapter 2 of Clean Code

 

The Art of Naming: Chapter 2 of Clean Code

After establishing why clean code matters in Chapter 1, Robert C. Martin dives into one of the most fundamental skills in programming: choosing good names. Chapter 2, "Meaningful Names," might seem simple at first, but it contains wisdom that separates amateur code from professional craftsmanship.

Names Are Everywhere

Before we get into the rules, consider this: names are everywhere in software. We name variables, functions, arguments, classes, packages, source files, and directories. We name and rename constantly. Given how much naming we do, we might as well do it well.

Use Intention-Revealing Names

The name of a variable, function, or class should answer three big questions: why it exists, what it does, and how it's used. If a name requires a comment to explain it, the name doesn't reveal its intent.

Consider this example in Ruby:

ruby
d = 10 # elapsed time in days

Versus:

ruby
elapsed_time_in_days = 10

Or in TypeScript:

typescript
const d: number = 10; // elapsed time in days

Versus:

typescript
const elapsedTimeInDays: number = 10;

The difference seems trivial, but it compounds across thousands of lines of code. Good names make code self-documenting.

Avoid Disinformation

Programmers must avoid leaving false clues that obscure the meaning of code. Don't refer to a grouping of accounts as account_list unless it's actually a List or Array. If it's not, account_group or just accounts would be better.

ruby
# Misleading - not actually an array
account_list = Account.where(active: true)

# Better - accurately describes what it is
accounts = Account.where(active: true)
typescript
// Misleading - this is a Set, not an Array
const accountList: Set<Account> = new Set(activeAccounts);

// Better
const accountSet: Set<Account> = new Set(activeAccounts);

Beware of names that vary in small ways. How quickly can you spot the difference between XYZControllerForEfficientHandlingOfStrings and XYZControllerForEfficientStorageOfStrings? These similar names create cognitive load and opportunities for bugs.

Make Meaningful Distinctions

If you have two things that need different names, make sure the names actually convey different meanings. Number-series naming (a1, a2, a3) is the opposite of intentional naming. So is noise words.

What's the difference between ProductInfo and ProductData? Between Customer and CustomerObject? These distinctions are meaningless. Noise words like Info, Data, Object, Manager, Processor don't add clarity, they just add clutter.

ruby
# Meaningless distinction
class ProductInfo
end

class ProductData
end

# Better - use one clear name
class Product
end
typescript
// Noise words that add no meaning
interface CustomerObject {
  name: string;
}

class CustomerManager {
  // What does "Manager" actually do here?
}

// Better - clear and direct
interface Customer {
  name: string;
}

class CustomerRepository {
  // "Repository" is a known pattern
}

Use Pronounceable Names

This might seem obvious, but it makes a huge difference. If you can't pronounce a name, you can't discuss it without sounding like an idiot.

Compare:

ruby
genymdhms = Time.now

With:

ruby
generation_timestamp = Time.now

Or in TypeScript:

typescript
const genymdhms: Date = new Date();

Versus:

typescript
const generationTimestamp: Date = new Date();

Which one can you actually say out loud to a teammate?

Use Searchable Names

Single-letter names and numeric constants have a particular problem: they're nearly impossible to search for. If you're using e as a variable name, try searching for it in a large codebase. Good luck.

ruby
# Hard to search for
users.select { |u| u.age > 5 }

# Better - searchable and meaningful
MIN_ADULT_AGE = 18
users.select { |user| user.age > MIN_ADULT_AGE }
typescript
// Hard to search for
const filtered = users.filter(u => u.age > 5);

// Better
const MIN_ADULT_AGE = 18;
const adultUsers = users.filter(user => user.age > MIN_ADULT_AGE);

Martin suggests that the length of a name should correspond to the size of its scope. If a variable is only used in a small loop, i might be fine. But if it has a larger scope, it needs a more descriptive name.

Avoid Mental Mapping

Readers shouldn't have to mentally translate your names into other names they already know. A single-letter variable name is fine for a loop counter, but using r for the "lowercase version of a URL with the host and scheme removed" forces readers to keep a mental map.

ruby
# Forces mental mapping
r = url.downcase.gsub(/^https?:\/\/[^\/]+/, '')

# Clear and direct
normalized_path = url.downcase.gsub(/^https?:\/\/[^\/]+/, '')

Smart programmers write code that others can understand. Professional programmers write clarity.

Class Names and Method Names

Martin provides clear guidance here:

Classes and objects should have noun or noun phrase names like Customer, WikiPage, Account, or AddressParser. Avoid words like Manager, Processor, Data, or Info in class names. A class name should not be a verb.

ruby
# Good class names
class Customer
end

class OrderProcessor # Sometimes "Processor" is acceptable if it truly processes
end

class PaymentGateway
end
typescript
// Good class names
class Customer {
}

class InvoiceGenerator {
}

interface UserProfile {
}

Methods should have verb or verb phrase names like post_payment, delete_page, or save. In Ruby, follow convention with snake_case for methods. In TypeScript, use camelCase.

ruby
# Good method names
def post_payment
end

def delete_page
end

def calculate_total
end

# Accessors and predicates
def active?
end

def total
end

def total=(value)
end
typescript
// Good method names
function postPayment(): void {
}

function deletePage(): void {
}

function calculateTotal(): number {
}

// Accessors and predicates
function isActive(): boolean {
}

function getTotal(): number {
}

function setTotal(value: number): void {
}

Pick One Word Per Concept

Pick one word for one abstract concept and stick with it. It's confusing to have fetch, retrieve, and get as equivalent methods in different classes. Choose one and use it consistently.

ruby
# Inconsistent - pick one!
class UserRepository
  def fetch_by_id(id)
  end
end

class OrderRepository
  def retrieve_by_id(id)
  end
end

class ProductRepository
  def get_by_id(id)
  end
end

# Better - consistent vocabulary
class UserRepository
  def find_by_id(id)
  end
end

class OrderRepository
  def find_by_id(id)
  end
end

class ProductRepository
  def find_by_id(id)
  end
end

Similarly, don't use the same word for two purposes. If you have add methods that create a new value by adding two values, don't use add for a method that puts a single value into a collection. Use append or push instead.

ruby
# Confusing - "add" used for different purposes
def add(a, b)
  a + b
end

def add(item)
  @items << item
end

# Better - distinct names for distinct operations
def sum(a, b)
  a + b
end

def append(item)
  @items << item
end

Use Solution Domain Names

Remember that the people reading your code are programmers. Go ahead and use computer science terms, algorithm names, pattern names, math terms. The name AccountVisitor means something to a programmer familiar with the Visitor pattern. Use technical names when appropriate.

ruby
class OrderDecorator
  # "Decorator" is a known pattern
end

class UserFactory
  # "Factory" is a known pattern
end

class EventObserver
  # "Observer" is a known pattern
end
typescript
class CacheStrategy {
  // "Strategy" is a known pattern
}

class DatabaseAdapter {
  // "Adapter" is a known pattern
}

class CommandQueue {
  // "Queue" is a known data structure
}

Use Problem Domain Names

When there's no programmer-ese for what you're doing, use the name from the problem domain. At least the programmer who maintains your code can ask a domain expert what it means.

ruby
# Healthcare domain
class PatientAdmission
end

class DiagnosisCode
end

# Financial domain
class LedgerEntry
end

class ReconciliationReport
end

Add Meaningful Context

Imagine you see variables named first_name, last_name, street, city, state, and zipcode. You can infer they're part of an address. But what if you just see the variable state in a method? Adding context helps: addr_state is better, but creating an Address class is best.

ruby
# Context through grouping
class Address
  attr_accessor :street, :city, :state, :zipcode
end

# Now it's clear what "state" means
address = Address.new
address.state = "MN"
typescript
// Context through typing
interface Address {
  street: string;
  city: string;
  state: string;
  zipcode: string;
}

// Clear context
const userAddress: Address = {
  street: "123 Main St",
  city: "Minneapolis",
  state: "MN",
  zipcode: "55401"
};

Don't add gratuitous context though. If you're building a "Gas Station Deluxe" application, prefixing every class with GSD is overkill. Shorter names are generally better than longer ones, as long as they're clear.

The Hardest Thing in Programming

Phil Karlton famously said there are only two hard things in Computer Science: cache invalidation and naming things. Chapter 2 of Clean Code won't make naming easy, but it provides a framework for making better naming decisions.

Key Takeaways

  1. Choose names that reveal intent and make code self-documenting
  2. Avoid misleading names and meaningless distinctions
  3. Make names pronounceable and searchable
  4. Use consistent vocabulary throughout your codebase
  5. Class names should be nouns, method names should be verbs
  6. Pick one word per concept and stick with it
  7. Use technical terms when appropriate, domain terms when not
  8. Add context through class structure, not prefixes

Practical Application

The next time you write code, pause before naming something. Ask yourself: will another developer (or future you) understand what this is without reading the implementation? If the answer is no, take a moment to find a better name. It's an investment that pays dividends every time someone reads that code.


What naming conventions have you found most helpful in your projects? Share your thoughts below!

Clean Code Chapter 5: Formatting

  Clean Code Chapter 5: Formatting Code formatting might seem like a trivial concern compared to architecture or algorithms, but Robert C. M...