Concept Design
Software designed at the level of the ideas it asks its users to hold — the units of behavior a person learns once and reuses everywhere — rather than at the level of features, screens, or endpoints.
The thing I keep coming back to, when something in a piece of software
suddenly clicks for someone, is how rarely the moment has anything
to do with the visible interface. Buttons, layouts, copy — these
matter, but they are usually not what is being learned. What is being
learned is a small, durable idea: that this app has a thing called a
Reminder, that a Reminder belongs to exactly one List, that a
List can be Shared and a Reminder cannot. Once that handful of
ideas lands, the user navigates screens she has never seen before and
is right more often than wrong.
The phrase I use for this layer of design, following Daniel Jackson, is concept design. His 2021 book The Essence of Software is the cleanest statement of the idea I know, and the more recent Beyond Objects essay sharpens the version of it I find most useful. The lineage runs further back than either: recognizable ancestors appear in entity-relationship modeling, in Michael Jackson’s JSD, in the formal-methods work on Z and Event-B, in Eric Evans’s domain-driven design, and more diffusely in the working culture of any team that has ever discovered, halfway through a project, that the right move was to rename half the entities. What concept design adds, on top of those traditions, is a name and a practice. A concept is a small, free-standing unit of behavior with a stated purpose, an inventory of actions, a small piece of state, and an operational principle that says how the actions hang together. A piece of software, in this view, is not a network of objects calling and inheriting from each other; it is a small set of concepts, each minding its own business, with their interactions stitched together explicitly rather than buried inside method calls.
The argument in one paragraph
Most software, in the way it is usually built, is described twice. There is the user-facing description — what the marketing site claims, what the help articles explain, what the user mutters to a colleague — and there is the implementation description, in code and schemas. Concept design proposes a third layer, sitting between the two and authoritative over both. A concept is named, has a stated purpose, has a small inventory of actions, and behaves the same way wherever it appears. The promise is that if the concepts are right, the screens write themselves and the code follows; if the concepts are wrong, no amount of UI polish or refactoring will rescue the product.
Why I find it useful
The first thing concept design gave me was a precise vocabulary for a complaint I had always had about feature lists. A feature is a marketing artifact; it is what gets announced. A concept is what the user has to internalize to use the feature without help. Two products with the same feature list can have wildly different concept loads, and the heavier one tends to lose, even if it is technically more powerful. Concept count — how many distinct ideas the user has to hold in order to be fluent — turns out to be a more honest measure of complexity than line count or screen count.
The second thing it gave me, slower to arrive, was a discipline of restraint. Each new concept is a thing the user must learn, a thing the documentation must explain, a thing future features must remain consistent with, and a thing the codebase must keep coherent under change. Adding a concept is the most expensive move in the design budget. Most products spend that budget without noticing they are spending it.
What a concept actually is
A concept has four parts and one virtue. The four parts are a name,
a purpose expressed in a sentence or two, an operational
principle (an archetypal scenario showing how the concept’s actions,
performed in some order, deliver on the purpose), and a small piece
of state together with the actions that read and write it. The
virtue, harder to legislate but easier to feel, is independence. A
concept does not refer to other concepts, does not call them, and
does not import their types. A concept named Upvote knows about
upvotes — that is more or less the whole point. It treats users as
anonymous identities and posts as anonymous things-that-can-be-voted-on,
and it has nothing to say about karma, ranking, or moderation, even
though all of those will, in any real product, want to ride along on
top of it.
A sketch makes this less abstract. An Upvote concept might have the
purpose register a user’s positive response to a post; an
operational principle along the lines of after a user upvotes a
post, the post is counted as upvoted by that user, and the user can
later remove the upvote; a piece of state consisting of a single
relation upvoted between users and posts; and two actions,
upvote(user, post) and unvote(user, post), with the obvious
preconditions about whether the upvote already exists. That is the
entire concept. It is small enough to fit on an index card. It says
nothing about whether the post is by the same user (a concern that
belongs to a separate SelfVotePolicy synchronization, if there is
one), nothing about how upvotes feed into ranking, nothing about
whether votes are public. Almost the entire interesting work of an
aggregator product can be expressed by composing twenty or so
concepts of this size, and almost any product, examined honestly,
will turn out to use some version of Upvote, Karma, Comment,
Following, and a small number of others, even if the team has not
given them those names.
The work of building a system therefore has two phases that look
quite different from the usual ones. The first is identifying the
concepts: writing out the small inventory of independent behaviors
the system actually traffics in, with their purposes stated honestly
and their actions named. The second is connecting them. Concepts do
not call each other; they are coordinated externally, by
synchronizations — declarative rules that say things like “when an
Upvote happens on a post by a user in good standing, increment the
author’s Karma.” The synchronizations are where the application’s
specificity lives. The concepts themselves are meant to be reusable
from one product to the next, the way a well-designed library
is — though, in practice, every team’s Upvote ends up subtly
different from every other team’s, which is one of the open
questions further down.
How concepts differ from objects
The most useful contrast is the one with the object-oriented
decomposition, and it took me a while to see why it cuts so deep. The
instinct of object orientation is that the modules of a system should
match the things in its problem domain: a class for User, a class
for Reservation, a class for Restaurant, with the actions those
things participate in distributed among them as methods. The trouble
is that almost no interesting action belongs to a single thing. A
reservation involves a user, a slot, a restaurant, and a clock;
assigning the action to one of them is arbitrary, and the
consequences of the arbitrary choice cascade. Functionality that
ought to live together gets fragmented across classes, and modules
that ought to be reusable get fingerprinted by the particular
collection of concerns they happened to encode.The short version is that the two ideas at the heart of
object orientation — that actions belong to objects, and that the
objects should be the entities of the problem domain — are each
defensible alone but ruinous together. The combination forces every
non-trivial computation to find a home it does not really
fit.
A concept structure makes the move that object orientation declines
to make. The unit of decomposition is not the noun in the problem
domain but the named behavior. A user, a slot, and a reservation can
all participate in the Reserving concept; they can also
participate, without contradiction, in UserAuthentication,
Karma, Availability, and any number of other concepts, each with
its own relations and its own actions. The same individual is allowed
to appear in many concepts at once, with different roles in each,
which turns out to be exactly what real domains require and exactly
what classes in an object-oriented language make awkward. The cost of
the move is one extra layer of explicitness — the synchronizations —
that object-oriented code tends to leave implicit in method calls and
inheritance hierarchies. In return, the modules become reusable in a
way they almost never are otherwise, and the cross-cutting concerns
that produce the worst of object-oriented code (the layered
decorators, the aggregate roots, the service objects designed to
patch over fragmentation) stop being needed.
Where the framework gets uncomfortable
Concept design is not a free lunch, and naming where it is awkward seems more useful than pretending it is uniform. Anything that depends on the composite structure of an object — fingerprinting, hashing, serializing across a wire, computing a checksum over a bounded chunk of state — does not factor cleanly into independent concepts; it wants the whole noun, with all its relations, in one place. Performance-critical code with tight invariants across many relations can be hard to express without smuggling in a coordinating module that starts to look suspiciously like a class. And the discipline of “concepts do not call each other” is easier to state than to enforce: the temptation to let one concept peek at another’s state, just this once, is the same temptation that produces the dependency tangles concept design is meant to prevent.
None of these are fatal, and most have known answers in the literature, but they are the places where the framework asks the most of its practitioners. They are also where the easy version of concept design — concepts as a vocabulary, not as a discipline — quietly fails. Treating concepts as a way to describe a system you have already built in some other style is fine, and probably an improvement; treating them as the unit you actually build with takes considerably more work, and most of the interesting questions in this area are about how to do that without giving up the discipline along the way.
Open questions
The questions that hold my attention sit in the gap between the formal account of concepts and the practice of shipping software made of them.
Discovery is the one I find most pressing. The literature describes what good concepts look like once you have them, but the practice of finding the right concepts for a particular product is mostly tacit. Some teams converge on a clean inventory quickly; others go through three or four redesigns before the structure becomes obvious, and the obvious structure, in retrospect, was always there. I do not yet have a clear view of what makes the difference, or whether the practice can be taught the way, say, normal-form database design can. My working suspicion is that good concept discovery looks more like literary editing than like analysis — a matter of finding the level at which the system wants to be described — but I would like that suspicion to either harden into a method or be replaced by a better one.
Evolution sits next to discovery and is at least as hard. Concepts are supposed to be the long-lived furniture of a product; they are meant to outlast features. In practice, products change and so do their concepts. What does it look like to deprecate a concept gracefully, to split one concept into two when its purpose has quietly forked, or to merge two concepts that turned out to be aspects of the same thing? Version control for code is well developed. Version control for the conceptual structure of a product mostly is not, and the absence shows up as quiet drift: the concept’s name stays the same while its purpose gradually changes, and nobody notices until a new feature collides with the old behavior.
The link to Agent-Compatible Systems is the question I think about most often. A concept, with its named purpose, its small action set, and its bounded state, is something close to what a model-based agent actually needs in order to read and act on a system. The current generation of tool-use APIs and capability descriptions is, charitably, a worse version of what a concept catalog could be. Whether systems built concept-first turn out to be agent-compatible by accident, or whether there is more design work between the two than I currently see, is something I would like an answer to. The provisional answer seems to be “yes, but the synchronizations are the hard part” — concepts give the agent the vocabulary, but the rules that coordinate them are where the application’s intent actually lives, and exposing those rules legibly is its own problem.
There is also a less precise question, raised by Malleable Software. If a concept is a small unit of behavior a user could in principle understand, then in principle a user could also adjust one — change a precondition, add a relation, replace the operational principle with something closer to her own situation. What it would mean for the concept inventory of a product to be visible and editable from the inside, the way a spreadsheet’s formulas are visible and editable, I do not know. But the question seems like the right kind to ask, and the answer probably reshapes a fair amount of what end-user programming looks like.A related question, raised sharply by the recent flood of LLM-generated code, is whether concept-structured systems are easier for models to extend without accumulating debt. The early experiments are encouraging, but the verdict is not in.
The most diffuse question is about shared vocabulary. Concepts are
meant to be reusable across products: Reservation could in
principle be the same concept whether the product is a restaurant
booker or a meeting-room app. In practice, every team reinvents its
own slightly-different version of the same thing, and the result is a
concept that almost matches everyone else’s Reservation and
exactly matches nobody’s. Whether a public catalog of well-formed
concepts could change this — and whether such a catalog would survive
the social pressures that fragment standards in every other corner of
software — is an open empirical question, and one I would like to see
attempted before deciding whether it is hopeless.
A working stance
The aspiration here is modest and a little contrarian. Most software-design discourse is concerned with surfaces: what the screen looks like, what the API returns, what the model emits. Concept design says the most important decisions are made one layer below all of those, in the inventory of small, durable ideas the product asks its users and its developers to share. Get those right, and the surfaces have a chance of being honest. Get them wrong, and no amount of design polish will hide it for very long.
What I find I keep doing, when I read other people’s products now, is reverse-engineering the concept inventory before I look at the screens. The exercise is often unkind to the product. Concepts that should have been one are two; concepts that should have been separate are tangled; concepts the marketing site treats as central turn out, on inspection, to have no operational principle anyone could state. None of this is visible in a feature comparison, and most of it is invisible to the team that built the product. That gap — between what a product is taken to be and what it actually is, at the level the user has to learn — is most of what concept design is for.