Abstraction Is a Contract

In day-to-day software development, the word “abstraction” comes up constantly.

In object-oriented languages such as Java or Kotlin, you can design classes, interfaces, and abstract classes so the architecture itself becomes an exercise in abstraction.

Abstraction often appears in the early phases of design, but we also have to keep it in mind when building features.

In this article, I would like to dig into what “abstraction” means in the context of software development.

What the word “abstraction” means

The term abstraction is, fittingly, an abstract word.

Even when someone says “make it more abstract,” it is surprisingly hard to explain what that entails.

Nuance-wise, it can mean:

There is a similar term, “commonization” (making things common).

I see the difference between abstraction and commonization as a matter of conceptual level.

Commonization is the task of gathering shared elements regardless of what conceptual layer they belong to.

Abstraction, by contrast, operates at a higher conceptual layer. It bundles only the essence and cuts it out from everything else.

Commonization in software

When we write programs, we are naturally conscious of commonizing logic.

That is because it is almost instinctive to understand that having similar logic scattered everywhere makes maintenance harder.

Take a simple example: you need logic to convert amounts to tax-inclusive prices. That logic might be needed across the entire system, not just on a single screen. If you sprinkle it around in many places and the tax rate changes, you suddenly have to modify lots of code.

In Japan there is a reduced tax rate system where groceries and other items have different consumption tax rates, so simply sharing the tax rate constant is not enough.

Because this tax rate is likely to change in the future, commonizing the logic reduces the number of places to update and makes it easier to identify the impact.

In that sense, it is relatively easy to decide when to commonize.

Abstraction, on the other hand, is harder to grasp intuitively than commonization.

Abstraction in software

We can easily be mindful about commonizing logic at the feature level, but abstraction feels nebulous.

That is because abstraction handles abstract things—especially in software development.

Let’s use a DDD/CA (Domain-Driven Design / Clean Architecture) setup to illustrate.

Assume our system has four layers: domain, infrastructure, use case, and application.

We impose the following dependencies:

This rule cleanly separates responsibility by layer.

Each layer is an abstract concept; there is no tangible “layer” entity.

Abstraction is the technique we use to enforce this rule.

Let’s consider this using OOP, which plays nicely with DDD/CA.

The domain layer is made up of plain classes and interfaces. It holds no concrete implementations.

data class XxxEntity {
  ...
}

interface XxxRepository {
  ...
}

The infrastructure layer holds implementations. It is allowed to depend on the domain layer.

class XxxRepositoryImpl : XxxRepository {
  override ...
}

The use-case layer may depend on the domain layer but must not depend on infrastructure, so dependencies are injected from the outside.

class XxxUseCase(xxxRepository: XxxRepository) {
  ...
}

The application layer depends only on the use-case layer.

class XxxApp(xxxUseCase: XxxUseCase) {
  ...
}

Grouping responsibilities like this at the conceptual level is what I consider abstraction.

But this raises a problem:

Isn’t the application layer still depending on the domain layer through the use case? Dependency here means we might have to change dependent components when the thing they rely on changes.

To avoid that, we add an abstraction layer to the use-case layer and inject dependency into the application layer from the outside.

In other words:

interface BaseUseCase {
  ...
}

class XxxUseCase(xxxRepository: XxxRepository) : BaseUseCase {
  override ...
}

class XxxApp(useCase: BaseUseCase) {
  ...
}

Now we enforce the contract that only the methods defined in the interface may be invoked. The interface is the contract, and thus the abstraction rule. In other words, we lock in the responsibility without locking in the implementation.

This is the real power of abstraction in software development.

Why abstraction helps

Finally, let’s discuss why abstraction is so often recommended.

In my opinion, abstraction is a cost for small systems, small teams, or solo development.

Abstraction tends to increase implementation cost and can raise cognitive load.

This is what people call over-abstraction.

On the other hand, in large systems that juggle many concepts, or in environments where multiple development teams collaborate, designing with abstraction works well.

In those scenarios, abstraction can reduce both implementation cost and cognitive load.

So it is wrong to declare abstraction a cost across the board. You have to choose when to use it case by case.

It is easy to imagine why abstraction becomes a cost in small contexts, so I will skip that and focus on why it can reduce cost at scale.

When layer responsibilities are clear:

Ultimately, good software design comes down to two things: maintainability and low cognitive load.

Maintainability encompasses reliability, availability, and extensibility—in short, how easy something is to change.

Large-scale development means many people are changing many parts in parallel. Ensuring those components do not affect each other, or that any impact is obvious, goes a long way toward long-term maintainability.

Abstraction is the tool for that. Or, if you prefer, think of it as a set of rules or contracts.

I believe the stricter those rules or contracts are, the more they contribute to future extensibility, because they lower cognitive load.

Loose rules and high freedom might sound like they lead to greater extensibility, but I argue the opposite.

That is why statically typed languages ultimately scale better than dynamically typed ones, even if their initial development speed is slower: the type system is a contract. Even for variables, immutable types scale better than mutable ones. Dependencies scale better when enforced at the layer level instead of becoming a tangled mess.

These practices shrink the blast radius of changes and thereby lower cognitive load.

In conclusion, abstraction is a contract. In large-scale development it ultimately lowers cost—abstraction is an investment in the future. In small-scale development, abstraction can instead be a cost. That is how I see it.

Evaluate how you use abstraction through the lenses of:

In closing

In this post we examined “abstraction” within software development. You only truly learn whether a design works once it has been run and maintained.

You have to make choices at design time, and the “right” answer is unknown then. That uncertainty is part of the challenge—and the fun.

I often see systems that simply mimic some popular architecture, but what matters is choosing a structure that matches your system’s outline at design time. It is not a good idea to assume one architecture can solve everything.