atelier

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.