DEV Community

Ian Johnson
Ian Johnson

Posted on

What is the domain and why is it so important?

Open a codebase you've never seen before and try to figure out what it does. Sometimes you stare at imports, configuration, error handling, and connection pools for an hour before you find the file where something actually happens: the file where money moves, an order ships, a patient gets scheduled. That file is the domain. Everything else is plumbing.

The domain is the part of your software that talks about the problem you're solving, in the terms of the people who have the problem. A banking system's domain is accounts, transactions, balances, transfers, interest accrual, overdrafts. A clinic's domain is patients, appointments, providers, visits, prescriptions, claims. A logistics company's domain is routes, shipments, drivers, manifests, exceptions. The domain has nothing to do with whether the database is Postgres or DynamoDB, whether the API is REST or gRPC, whether the deploy target is Kubernetes or a single VM. Those are real engineering decisions and they matter, but they aren't the thing. The thing is the business.

This distinction is hard to feel until you've worked on a codebase where it's been ignored. In such a codebase, an "order" isn't an object that knows how to be placed and canceled — it's a row that someone retrieved with a SQL query, mutated with a few setters, and saved with another query, all tangled with logging, retries, and authentication checks. To understand what happens when an order is placed, you have to read everything. The business logic is everywhere and nowhere.

In a codebase where the domain has been taken seriously, you can open Order.place() and read what placing an order actually means: stock is reserved, the customer is charged, a confirmation event is emitted, the order enters a pending state. You learn this without learning anything about HTTP, SQL, or message brokers. That's not an accident. That's design.

Build a language, then program in it

There's a beautiful pattern Abelson and Sussman teach in Structure and Interpretation of Computer Programs: you don't solve your problem directly in the base language you started with. You use that base language to build a vocabulary suited to the problem, and then you express the solution in that new vocabulary. SICP keeps doing this, building little interpreters, evaluators, and abstractions, until "writing the program" looks more like "writing down the answer."

This is the same instinct as designing a domain. You're not really writing Python or TypeScript when you write the domain. Instead, you're writing the language of accounts, orders, and shipments, using Python or TypeScript as the substrate. The names of your types and functions are the vocabulary; the relationships between them are the grammar. Done well, the result reads almost like prose written by someone who understands the business.

DSLs are this idea, made explicit

A domain-specific language takes the pattern to its conclusion: design an actual little language for the problem and write the solution in it. SQL is a DSL for querying relational data. Regular expressions are a DSL for matching text. Make is a DSL for declaring build dependencies. Each one trades generality for the ability to say what it needs to say with almost no ceremony.

You don't need to invent a parser to get the benefits of DSL thinking. An "internal DSL" (sometimes called a fluent interface or an embedded DSL) is just code in your host language that has been shaped to read like the domain. When a routing library lets you write route("/users/:id").get(handleUser), that's an internal DSL for describing HTTP routes. When a testing library lets you write expect(user).toHaveRole("admin"), that's an internal DSL for expectations. The host language is still there, but it has been bent until the surface of the code is domain vocabulary, not language vocabulary.

Hexagonal architecture and the domain at the center

Alistair Cockburn's hexagonal architecture (also called ports and adapters) gives this idea a concrete structural form. The domain sits at the center of the application. Around it are "ports": abstract interfaces describing what the domain needs from the outside world ("save this order somewhere," "notify the customer somehow") and what the outside world can ask of the domain ("place this order"). Around the ports are "adapters": the concrete implementations that connect ports to real technology, like a Postgres adapter, an HTTP adapter, or an SQS adapter.

The crucial property is the direction of dependency. The domain does not import the database client. The domain does not know that HTTP exists. The adapters know about the domain; the domain does not know about them. This means you can swap Postgres for DynamoDB, or REST for gRPC, or your message queue for a different message queue, without touching the part of the code that describes how the business works. And because the domain has no infrastructure dependencies, you can test it with plain function calls, no test containers required.

The test: can a domain expert read it?

The practical heuristic for all of this is readability and discoverability. Someone who understands the business — not necessarily a developer, but at least someone who knows what an order is and what it means to place one — should be able to open the domain code and roughly follow what it does. They shouldn't need to know what an ORM is. They shouldn't have to mentally filter out exception handling, transaction management, and retry logic. The names should be the names they already use. The operations should be the operations they already perform.

Discoverability is the other half. When a new developer joins the team and asks "where does the actual order-placing logic live?", there should be a short, satisfying answer: it lives here, in this folder, in these files. Not "well, some of it is in the controller, some in the service, some in the database stored procedures, and a critical piece is in this cron job." If the domain is scattered, nobody will fully understand it, and changes will be terrifying.

Everything technology-specific (the SQL, the HTTP status codes, the JSON serialization, the retry policies, the cache invalidation) gets pushed behind an abstraction. Not because those things are unimportant; they're often where the bugs live. But because mixing them with domain logic makes both harder to think about. Separated, each can be reasoned about on its own terms. The domain says what the business does. The adapters say how the technology cooperates. Both are clearer for the separation.

That's why the domain matters. It's where the value lives. It's where the bugs that cost real money live. It's what you're actually being paid to get right. The framework will be replaced. The database will be migrated. The cloud provider will be swapped. But the meaning of an order, the rules around a transaction, the constraints on a schedule? Well, those are the substance of the software, and they deserve to live in code that says, plainly and centrally, what they are.

Top comments (0)