For someone new to the craft of software development, it can look simple at first. You connect a few things — a few functions here, a few functions there — and voilà! You have working software.
Only… that's not it. There's more to a good piece of software than just working software. That's where this article comes in. I want to guide you on where to start thinking about your system so it grows to be a magical source of happiness, not an endless pit of sorrow.
The Domain: The Core of Your System
You may have heard the word "domain" here and there. I don't know where you are in the endless quest that is understanding what a domain is, so I'm going to assume the worst and start from the very foundation.
The domain is the core of your system. It's the most basic representation of the world your system wants to model. Let's say you're creating software to manage a kitchen: the domain is all the concepts and rules that make up your relevant idea of a kitchen. A chef, a pan, a dish — all that.
The domain is a model, and like any model, it's not perfect. Nor is it trying to be. The goal of your model is to represent something to a certain degree of accuracy so you can work with it. If you look at a world map, there's a gazillion details left out, but that's not the point. You use the map to, let's say, drive from your home to your friend's — not to list all rivers in Equatorial Guinea. If you did want to do that, you'd either go to Equatorial Guinea and visit all rivers yourself (using the real world), or you'd use a map of rivers in Equatorial Guinea (a working model).
The domain is not only a set of concepts. It's also the rules that determine how those concepts interact — and how they shouldn't. It determines what's valid and what's not. A model of a football match in a football game will use the rule "the ball model should have a gravitational force of 9.8m/s²", which matches the real gravity of earth pulling the ball down. Those invariants of what's right and what's wrong live in the domain.
The domain is your core — that's why, whenever you used to start programming in, let's say, an OOP language, you'd be taught how to model simple objects. The domain is meant to be simple in implementation, but not necessarily in semantics. If your business requires a complex model, so be it. That's the nature of what you're working on.
An important concept comes into play here: cohesion. You want things that belong together, together. You want to think of your domain in a cohesive way. It's important for you to understand how things work together, identify those "boundaries" where things make sense as a group, and put things that don't belong in that boundary into another boundary.
If you've ever worked in a codebase organised by feature folders, or heard someone talk about microservices, you've already bumped into this idea — even if nobody called it a boundary at the time.
You don't have to be a domain expert. The role of the engineer was never to be the expert on the domain they are modelling — you're the bridge between the people who really do know about that world and the software. Your job is to model things, understanding them in the process. Not to know everything.
The domain should live in isolation. You should be able to test things in isolation (unit tests) and their interactions in isolation (integration tests). Preferably, model your domain without using any external frameworks or libraries, unless that's absolutely necessary to make your life easier. Coupling to those external concepts will make it hard to change things when you need to, or to work on them independently.
The Application: What Your System Can Do
Now that you have a domain, what's next? Well, I hope you're creating this piece of software to solve a problem or to make the world a better place. That means your software is meant to do something, not only to exist as a model, right?
The "what can your software do?" goes in the application. The base concepts you work with should be the capabilities the system needs to have. It's always good to start from the person or system who will be using your software to understand the required capabilities (a.k.a. actors). "As a person who's hungry, I want to ask for a sandwich from the automated kitchen so I get lunch." That's a capability, sometimes called a use case. Forget about the technicality of the naming for now.
Good applications are coordinators. They define capabilities clearly and coordinate concepts in the model so it helps solve the problem at hand. The moment the application starts growing in complexity, developing its own concepts, or accumulating business rules inside of it — that's a smell. Something has gone wrong.
When thinking about your application, try to think of it in complete isolation from any framework, or even from how the actor will interact with it. A good application can be moved from one channel to another and still work just fine. Imagine a system that takes requests for an order and does something with them. You want to think of your application as something that takes a request from somewhere and coordinates whatever is needed to serve that request — whether that request comes through a phone call, an SMS, a WhatsApp message, or a physical letter that gets scanned. You shouldn't care about any of that. You should be able to completely isolate the outer layer of your system from its core capabilities.
Infrastructure: Where Your System Meets the World
We were just talking about the outer layer of your system — the infrastructure layer. Here's where things get physical.
Imagine you're building a new system accessible through an HTTP API. Your system's core should not care whether there's an API in front of it. The infrastructure layer is responsible for creating and exposing that API, and for making all the necessary transformations — translating what the API receives as a request into what the application requires. Think of it this way: the API might receive a JSON payload with a field called user_id. Your application doesn't know — or care — what JSON is. The infrastructure layer's job is to unpack that, validate its shape, and hand the application a proper PlaceOrderCommand with a typed UserId inside it. The application just sees a command. It has no idea an HTTP request ever existed.
You need to be able to completely drop the entire infrastructure layer implementation, swap it for another, and have the system still work as expected.
Architecture: Keeping the Layers Clear
The concept of these layers appears in several different ways across different architectural patterns. Is one better than the other? Not necessarily. It is still the engineer's responsibility to understand what sort of solution the system requires at its specific phase, and to deliver something that serves that purpose.
But it's important to understand that even in the smallest monolith or the biggest distributed system, these concepts — a clear, isolated core; an application that understands and serves the system's capabilities; and an infrastructure that allows the system to grow into different channels — are things that need to be identified and kept in mind when making decisions.
Most of the fault symptoms we see in software over time come from these layers not being clear enough, and concepts leaking from one into the other — causing all sorts of problems. Ever seen a SQL query living inside a React component? Or business rules scattered across API controllers? That's what leaking looks like in practice. It starts small — one shortcut, one "I'll fix it later" — and before long, you can't change your database without touching your UI.
Working With AI: Don't Outsource Your Thinking
When working with AI agents, it's very important for the human to take responsibility for architectural-level decisions. It's very tempting to let the AI choose what is best, and the fact that the agent sounds very confident about its decisions doesn't mean those decisions are right.
Remember: the agent will only draw on past experiences and will assume requirements and necessities that may not exist, or that may be different from what you actually need. It's therefore very important for the human to have accountability over these decisions and be prepared to change things in the future.
My advice is: start simple. Define your core with its bare minimum concepts and grow your system from there. Any system grown from the wrong domain will lack the conceptual flexibility it needs to evolve. Pay attention to it from the very start, and you'll find that thinking about use cases as the system grows becomes an evolutionary thing — not something that requires a full rewrite every time you need to make a change. Making changes should be your currency of maturity. A system is mature — and essentially good — when changes are cheap to make.
Final Thoughts
The role of the engineer is evolving. We should all be grateful for no longer needing to become experts on the latest technology, and instead focus on understanding the capabilities of our tools and using them to architect better solutions.
If this is the first time you've thought about these boundaries — good. It means your journey is taking you to a point where you need to start thinking at a higher level, and not worry too much about the smallest details. Agents will do their thing. You do your thinking.
