Better Problems: The Power of Abstraction

작성자

카테고리:

← 피드로
DEV Community · Jim McKeon · 2026-07-21 개발(SW)

Second in a series on practical software design.

Technical details are essential, but they’re not the solution

Which is clearer?

Here’s a simple user story for a lending library api:

As a library member, I want to borrow a book so that I can take it home.

And here are two code blocks that implement it. Take a few seconds to look at each. Which is easier to understand?

[HttpPost("borrow")]
public async Task<IActionResult> Borrow(int bookId, int memberId)
{
    var connectionString = _config.GetConnectionString("LibraryDb");
    using var conn = new SqlConnection(connectionString);
    await conn.OpenAsync();
    var cmd = new SqlCommand("SELECT * FROM Books WHERE Id = @id", conn);
    cmd.Parameters.AddWithValue("@id", bookId);
    var reader = await cmd.ExecuteReaderAsync();
    Book book = null;
    if (await reader.ReadAsync())
    {
        book = new Book();
        book.Id = (int)reader["Id"];
        book.Title = reader["Title"].ToString();
        book.DueDate = reader["DueDate"] as DateTime?;
        book.CheckedOutByMemberId = reader["CheckedOutByMemberId"] as int?;
    }
    reader.Close();
    if (book == null)
    {
        return NotFound();
    }
    // check if already checked out
    if (book.CheckedOutByMemberId != null && book.CheckedOutByMemberId != 0)
    {
        return BadRequest("already out");
    }
    // now get the member fines
    decimal fines = 0;
    var cmd2 = new SqlCommand("SELECT Amount FROM Fines WHERE MemberId = @m AND Paid = 0", conn);
    cmd2.Parameters.AddWithValue("@m", memberId);
    var reader2 = await cmd2.ExecuteReaderAsync();
    while (await reader2.ReadAsync())
    {
        fines = fines + (decimal)reader2["Amount"];
    }
    reader2.Close();
    if (fines > 0)
    {
        return BadRequest("has fines of " + fines.ToString());
    }
    // set due date to 2 weeks from now
    var due = DateTime.Now.AddDays(14);
    var cmd3 = new SqlCommand("UPDATE Books SET DueDate = @d, CheckedOutByMemberId = @m WHERE Id = @id", conn);
    cmd3.Parameters.AddWithValue("@d", due);
    cmd3.Parameters.AddWithValue("@m", memberId);
    cmd3.Parameters.AddWithValue("@id", bookId);
    await cmd3.ExecuteNonQueryAsync();
    // add to member checkout list
    var cmd4 = new SqlCommand("INSERT INTO MemberCheckouts (MemberId, BookId, CheckedOutOn) VALUES (@m, @b, @now)", conn);
    cmd4.Parameters.AddWithValue("@m", memberId);
    cmd4.Parameters.AddWithValue("@b", bookId);
    cmd4.Parameters.AddWithValue("@now", DateTime.Now);
    await cmd4.ExecuteNonQueryAsync();
    return Ok("borrowed");
}

Enter fullscreen mode Exit fullscreen mode

[HttpPost("borrow")]
public async Task<IActionResult> Borrow(int bookId, int memberId)
{
    var book = await _books.GetById(bookId);
    if (book is null) return NotFound();

    if (book.IsCheckedOut)
        return Conflict("Book is already checked out.");

    if (await _members.HasOutstandingFines(memberId))
        return BadRequest("Outstanding fines must be cleared before borrowing.");

    book.SetDueDate(_clock.Now.AddDays(LoanPeriodDays));
    await _bookCheckoutService.CheckOutBook(memberId, book);

    return Ok();
}

Enter fullscreen mode Exit fullscreen mode

If you’re like me, you chose the second. Why? It’s easier to parse because there’s less information in it (also, it uses more intuitive variable and method names, but that’s for another post). We have a limited capacity for detail, and usually only want enough to answer the question in front of us. Past that point, our capacity for analysis and critical thinking deteriorates. Less information in this case is always better.

The Case Against Detail

The second method is clearer because it abstracts the technical details that are in the first. The terms “abstracting” and “abstraction” are omnipresent in design literature: DDD, Clean Architecture, SOLID, etc., all assume you’re comfortable with it, but none of them describe how it works or how to get fluent in it.  And despite being foundational, it’s rarely taught explicitly. CS programs teach the language mechanisms of abstraction — abstract classes, interfaces, ADTs — but not how to apply it to requirements analysis or software design.

So we’ve seen how code that abstracts technical details is easier to process. But readability is the downstream benefit, the result of well designed software. The same act of substituting a symbol for a mess of detail does something more valuable earlier, when the code doesn’t exist yet and you’re building an understanding of the problem.

When you replace “open a connection, run this query, map these columns” with repository.GetSomething(), you’re not just hiding detail from a future reader: you’re deliberately hiding it from yourself, so your working memory is spent on the shape of the solution rather than the mechanics of data access. This is the real purpose of encapsulation. It gets taught as a mechanism but the reason it matters is almost never stated: holding complexity at bay so a mind can work above it.

You Already Abstract

This shift from trees to forest-first thinking can be hard for developers, but your mind already does it instinctively. Consider this: a friend just texted asking if you can meet for lunch tomorrow. Start assessing your schedule.

Most likely your thoughts didn’t follow a sequence like this:

  • open eyes
  • hit snooze
  • stretch, yawn, rub eyes
  • move one leg to the edge of the bed …

You automatically grouped the morning into a single block — “get up, get ready” — then checked it against the commitments that actually matter: a mid-morning meeting, the lunch window, your afternoon. You abstracted without trying, because processing a day as a hundred discrete physical actions would be paralyzing.

In effect you answered the question by skipping the details. The minutiae of your morning routine — every step of making coffee, every minute of your commute — aren’t just unnecessary, they actively get in the way. They consume the working memory you need for the actual problem: “am I free at noon?”

This is the same dynamic at work in code. The forty-line version of Borrow() forces you to hold connection strings, query parameters, and column mappings in your head before you can even see the end to end flow of the solution. At that level, the details are noise that obscure it — and worse, they let you make progress on implementation without ever arriving at a viable design. We already know this intuitively about everything except the code we write. The next section is about why.

Detail Or Die

The answer is that software development is extraordinarily complex. Just configuring an IDE, getting a debugger attached, or resolving your first merge conflict takes a real investment of time and energy. In the beginning, the details consume you because they have to — they’re the price of entry.

But after you’ve been writing code for a while, you start to feel the cost. The code is hard to follow, fragile to change, and difficult to debug without breaking something. That frustration is a signal: the detail-first habit that got you through the learning curve is now working against you. This is where learning to abstract starts to pay off.

How to Abstract

Abstraction is thinking in terms of what happens rather than how it happens. It’s holding a technical theme in your head instead of the mechanics that comprise it.

To get there, you have to short-circuit your mind’s detail-focused default — the reflex to reach for the connection string, declare a variable, calculate a result before you’ve nailed down the high-level flow. One way to force the switch is to write down, in plain prose, what your code will do before you implement it. To demonstrate, let’s construct a solution for the library API user story at the top of the post:

As a member, I want to borrow a book so I can take it home.

After giving it some thought, one possible sequence of events is:

  • Get the book from the app db
  • Verify it’s not already checked out
  • Verify the member’s fine balance is $0
  • Update the due date on the book
  • Add the book to the member’s checked-out list
  • Save changes

That’s the whole operation. Six lines, zero technical details. At this altitude you can hold the entire endpoint in your head at once, because you’re thinking in responsibilities, not implementation.

And because the solution is easy to express, it’s cheap to change. Look at the fine-balance check — we verify the member owes nothing, but we never checked whether the member is allowed to borrow another book at all. Most libraries cap how many items you can have out at once. It’s a big miss.

To fix it we add one line:

  • Get the book from the app db
  • Verify it’s not already checked out
  • Verify the member’s fine balance is $0
  • Verify the member is under their borrowing limit
  • Update the due date on the book
  • Add the book to the member’s checked-out list
  • Save changes

That edit cost a few keystrokes. Had we caught this three hundred lines into the implementation — a new query, a new branch, a new failure path threaded through code that assumed it wasn’t there — it would have cost an afternoon, and more likely than not, made the code less readable and flexible. We changed our mind for free because we abstracted first.

Learning From Experience

Keeping your eye out for where abstraction might have helped is another good way to build the skill. Often, duplication is the symptom. When you see a significant code block repeated across a class or file, ask yourself: what responsibility does this represent? This applies to new code too — when you’re writing something, ask yourself what existing code it resembles (even partially). If the answer is ‘a lot of things,’ you’re looking at a missing abstraction.

As you accrue experience with this mindset, you’ll start noticing shapes that repeat across systems — even systems in completely different domains. For example, a significant portion of CRUD API use cases implement some version of:

Validate input -> invoke service -> query data -> apply business rules -> construct response

These recurring shapes are the building blocks of design. I’ll dig into them — what the literature calls role stereotypes — in a future post. For now, the point is this: once you start thinking in abstractions, you stop solving every problem from scratch. It’s Legos vs a jigsaw puzzle.

Next Steps

The prose-first approach is a starting point — a thinking tool for breaking out of the detail-first habit and working at the right altitude. In upcoming posts I’ll discuss how to use abstraction to identify responsibilities, assign them to objects and incrementally build a working design. But none of that is possible until you can see the forest first.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다