A logbook for curious waters
← Back

The Working Vocabulary of Software Engineering

An experienced software engineer maps glowing modular structures at a drafting table while a lighthouse guides a sailboat through a storm.

The first program I wrote was a Visual Basic 3 form with a text box and a button. I was eleven. A person typed a name, clicked the button, and saw a message box:

Hello, <name>

The program itself was tiny. The idea inside it was enormous to me. Hello, stayed fixed; the name arrived from the person using the program. I could join something static to something supplied at runtime and produce a sentence that had never been stored in full.

That realization was a watershed. Beyond the greeting, I had found a general arrangement: decide which part must remain stable, leave a place for the part that may vary, then define how the two meet.

I taught myself by obsessively reading the help files that came with Visual Basic 3. After that came books and hours upon hours of experimentation. Much of my education in software followed a similar order: I encountered a pressure in code, worked out a response, and only later discovered its established name.

I kept meeting ideas before their names

About a year ago, a colleague used the word singleton. I already understood the classic shape he meant: a class controls access to one globally reachable instance. I simply had not attached the word singleton to it.

The missing word had not prevented me from understanding the design. Once I had the word, however, I could recognize the idea in documentation, discuss its costs without reconstructing it from first principles, and distinguish it from another meaning that software developers often give the same word.

The Singleton Pattern is the classic design pattern my colleague meant. The class controls its own construction, retains its sole instance, and gives the rest of the program a global way to reach it. The global access is not incidental. It is part of the pattern, and it is also the source of much of the pattern’s trouble: dependencies become easy to hide, mutable state can become everybody’s state, and isolated tests become harder.

A Singleton Lifetime is different. The part of startup code that creates and connects the application’s dependencies is sometimes called the composition root. It may create one instance of a service and pass that same instance to every consumer that requests it. A dependency-injection container can enforce the same lifetime. The consumers still receive the service explicitly; the service does not need to make itself globally reachable.

Both arrangements reuse one object. They do not create the same design.

That distinction is what a working vocabulary should make possible. A useful term compresses experience, but only after the people using it agree about the experience being compressed.

What the degree could name

Later I earned a master’s degree in computer science. The program was heavy on theory and proofs. The name computer science felt like a misnomer because the proof-heavy discipline I was studying looked less like a science of computers than a branch of applied mathematics concerned with computation and algorithms.

For my daily work as a software engineer, most of that curriculum was almost completely useless. The proofs were not trying to answer the questions my job kept asking: How do we make this software exist? How do we keep it working? How do we change it without breaking promises that users and other systems already depend on? How do several people work in the same codebase without turning every change into an expedition?

That judgment applies to the proof-heavy program I attended, not every course taught under the name of computer science. Computer science and software engineering both matter, but they do different work. Treating them as interchangeable obscures the strengths of both.

The program did give me another established name for an idea I already understood: a loop invariant is a fact that remains true before and after each pass through a loop. I had already reasoned with that kind of fact without knowing its established name. Learning the name did not retroactively create the understanding. It gave me a handle for examining it, verifying it, and discussing it with someone else.

At a broader level, the degree gave me some confidence.

Recognition before recital

Vocabulary can also be used badly. A list of terms easily becomes an entrance exam, a substitute for judgment, or a way to make familiar reasoning sound more rarefied than it is. A programmer who can recite Strategy but cannot notice an algorithm tangled with the code that uses it has learned a label and missed the work. The reverse case is common too: someone understands the pressure perfectly well but lacks the shared term that would let a team discuss it quickly.

A working vocabulary should shorten a useful conversation, not decide who belongs in it.

That principle changes the order of teaching. Instead of presenting a term and asking the reader to memorize its definition, begin with a situation the reader may already have met. What keeps changing? What must remain stable? Where has knowledge been duplicated? Which way should the source-code dependency point? What can fail halfway through?

Only then reveal the name.

The twenty recognitions below are not a miniature canon. They are a route into the habit: situation first, name second, boundary always. Real code can support several descriptions at once, so each answer identifies the name worth noticing rather than pretending that one label exhausts the example.

Twenty recognitions

Begin with the situation

Read each pressure before opening the term. Ask what is changing, what must remain stable, and what cost the likely response would introduce.

Notice the pressure

Before reaching for a pattern, learn to describe why the current shape is becoming difficult to change.

Nothing is broken, but duplicated knowledge or scattered responsibility makes the next change feel disproportionately risky.

The name worth noticing here is Code smell.

A code smell is a visible clue that a design may make some likely change difficult. It prompts investigation; it is not proof that the code is defective.

Boundary: Do not refactor merely to make a catalog entry disappear. Churn, defect risk, expected change, and the team's ability to read the current form all matter.

Read the examples and nearby terms
Responsibilities that change together live in different modules, while unrelated responsibilities crowd into one module.

The name worth noticing here is Cohesion and coupling.

Cohesion measures how strongly the responsibilities inside a module belong together. Coupling measures how much one part must know about another. Good boundaries usually strengthen cohesion and remove unnecessary coupling.

Boundary: Zero coupling is neither possible nor desirable. Prefer a few explicit dependencies with stable contracts over many layers of forwarding that only hide the same dependency graph.

Read the examples and nearby terms
Callers can reach inside an object and put it into a state the object itself would never permit.

The name worth noticing here is Encapsulation and information hiding.

Encapsulation lets a type control valid state transitions. Information hiding keeps volatile implementation decisions behind a stable public surface; public fields with ceremonial accessors provide neither benefit by themselves.

Boundary: Do not hide simple immutable data behind boilerplate solely to appear object-oriented. Encapsulate where a type must protect a rule or preserve freedom to change an implementation.

Read the examples and nearby terms
A design grows extension points for requirements nobody has evidence will arrive, making today's change harder.

The name worth noticing here is YAGNI: build for evidence, not imagination.

YAGNI - You Aren't Gonna Need It - warns against paying now for flexibility supported only by imagined requirements. Implement today's need cleanly and add variation when evidence appears.

Boundary: YAGNI does not excuse choices that are expensive or impossible to reverse. Preserve simple seams around high-risk external systems, data formats, and security boundaries.

Read the examples and nearby terms

Change the structure without losing the behavior

These names separate diagnosis, mechanical movement, and the tests that let a programmer move with confidence.

Working code needs a structural change, but callers should observe the same results, failures, and side effects.

The name worth noticing here is Refactoring.

Refactoring is a controlled change to code's internal organization that leaves its observable behavior intact. Its purpose is to make later changes easier, safer, or clearer.

Boundary: Do not use 'refactoring' as a synonym for a rewrite, cleanup campaign, or feature change. Separate behavior changes whenever practical so failures have a small search area.

Read the examples and nearby terms
One method validates input, calculates a price, charges a card, saves a record, and sends a receipt.

The name worth noticing here is Long Method.

A Long Method asks the reader to hold several phases, abstraction levels, or clusters of local state in mind at once. Line count is only a rough signal; mixed intent is the real smell.

Boundary: Do not extract tiny fragments whose names merely paraphrase their statements. Extract a coherent decision, calculation, or phase that improves the caller's story.

Read the examples and nearby terms
A coherent block of working code deserves a name and its own boundary, while observable behavior must remain the same.

The name worth noticing here is Extract Method.

Extract Method moves a coherent block into a well-named method and replaces the original block with a call. Fowler's language-neutral modern name is Extract Function.

Boundary: Choose a name that states intent, not mechanics such as ProcessData. Do not extract trivial fragments unless the name or reuse genuinely improves comprehension.

Read the examples and nearby terms
Adding one field requires coordinated edits to validation, persistence, serialization, and several callers in different files.

The name worth noticing here is Shotgun Surgery.

Shotgun Surgery means one conceptual rule has no clear owner, so changing it requires small coordinated edits across many files or modules.

Boundary: Centralization can create a new god service. Move the knowledge to the smallest cohesive policy, value object, or module that truly owns it.

Read the examples and nearby terms
The intended rules of legacy code are unclear. Tests record representative outputs before anyone changes the legacy code.

The name worth noticing here is Characterization and Golden-Master Tests.

A characterization test records behavior that uncertain legacy code exhibits today. A golden-master or approval test compares a larger output with a reviewed reference artifact.

Boundary: A snapshot can preserve bugs and make harmless timestamps, ordering, or whitespace look significant. Normalize only truly nondeterministic fields, review every approval diff, and keep fixtures small enough to understand.

Read the examples and nearby terms

Decide what varies and who owns it

The most useful design vocabulary names boundaries, dependency direction, and the responsibilities hidden inside familiar code.

One module changes for unrelated reasons: billing rules, report formatting, and database storage all converge there.

The name worth noticing here is Single Responsibility Principle.

The Single Responsibility Principle says a module should answer to one coherent source of change, usually one business capability or actor. It does not require one method per class.

Boundary: Splitting every operation into a separate class creates indirection without autonomy. Keep behavior together when it changes for the same reason and speaks the same domain vocabulary.

Read the examples and nearby terms
Business policy imports a concrete database client, so a storage choice dictates changes to stable rules.

The name worth noticing here is Dependency Inversion Principle.

The Dependency Inversion Principle places high-level policy and low-level details behind an abstraction shaped by the policy's needs. Source dependencies point toward the stable business rule.

Boundary: Do not create an abstraction that merely reproduces every method of a low-level library. The port should speak the policy's language and remain narrower than the detail it hides.

Read the examples and nearby terms
A class constructs its own repository and clock, making its dependencies invisible and controlled tests awkward.

The name worth noticing here is Dependency injection.

Dependency injection supplies an object's collaborators from outside rather than having the object construct or locate them. Required dependencies become visible, replaceable, and testable.

Boundary: A service locator hides dependencies behind ambient access and is not equivalent to injection. Keep container use near the composition root rather than resolving services throughout business code.

Read the examples and nearby terms
A class controls its own construction and gives the rest of the program a global way to reach its sole instance.

The name worth noticing here is Singleton Pattern.

Restrict a class to one instance and give callers a globally reachable way to obtain that instance. This is the classic Gang of Four pattern, not a dependency-injection lifetime.

Boundary: Global access can hide dependencies and turn mutable state into process-wide coupling. Define what “one” means, make first construction thread-safe when access can race, and do not use this pattern when explicit ownership is available.

Read the examples and nearby terms
Startup code creates one service and passes that same object explicitly to several consumers.

The name worth noticing here is Singleton Lifetime.

A singleton lifetime is an ownership rule: a composition root or container creates one instance and reuses it for the lifetime of that owner. Consumers can still receive the dependency explicitly.

Boundary: A long lifetime can accidentally retain mutable state, request data, disposable resources, or non-thread-safe collaborators. Match the lifetime to the resource and to every dependency it retains.

Read the examples and nearby terms
The workflow stays stable while one calculation, ordering rule, or routing policy must vary.

The name worth noticing here is Strategy.

Strategy encapsulates interchangeable algorithms behind one contract and lets a client or context select the policy to use.

Boundary: Do not create a class hierarchy for every one-line variation. Prefer named callables when they express the policy clearly, but use explicit strategy types when policies carry configuration, lifecycle, or several related operations.

Read the examples and nearby terms
A vendor library does the required job but uses names, types, or units the application should not spread through its own code.

The name worth noticing here is Adapter.

Translate an existing interface into the contract a client expects while preserving the underlying capability's meaning.

Boundary: Verify semantic compatibility, units, error behavior, and precision. An adapter cannot make fundamentally different meanings interchangeable.

Read the examples and nearby terms
A policy module imports framework and database details, letting volatile tools dictate its source-code dependencies.

The name worth noticing here is Architectural Boundaries and the Dependency Rule.

The dependency rule makes source-code dependencies point toward stable business policy. UI, database, network, and framework details depend on application or domain contracts rather than forcing core policy to depend on them.

Boundary: Interfaces added around every class create indirection without protecting a meaningful boundary. Place abstractions where volatility, ownership, testability, or policy direction justifies them, and enforce the rule mechanically.

Read the examples and nearby terms

Account for waiting, repetition, and hostility

Once work can wait, repeat, or receive untrusted input, correctness must include those conditions.

An operation waits on network or disk I/O. The execution thread should be free until the result arrives.

The name worth noticing here is Async/Await.

Async/await expresses suspension while an operation waits, preserving sequential-looking control flow without blocking an execution thread for the duration of asynchronous I/O.

Boundary: Async spreads through API boundaries and adds scheduling and lifetime concerns. Keep pure computation synchronous unless concurrency is intended, avoid sync-over-async, and always observe exceptions from started work.

Read the examples and nearby terms
A network request may arrive twice, but processing it twice must not create a second charge or order.

The name worth noticing here is Idempotency.

An idempotent operation has the same intended effect when the same logical request is processed more than once, even if individual responses differ.

Boundary: Define key scope, request matching, retention, in-progress behavior, and transaction boundaries. External effects beyond the local transaction still need an idempotent provider operation, outbox, or reconciliation.

Read the examples and nearby terms
A new data flow or privilege boundary creates abuse cases that the software's design must address before implementation hardens.

The name worth noticing here is Threat Modeling and Secure Design.

Threat modeling identifies assets, actors, trust boundaries, abuse cases, and plausible failures before implementation choices harden, then selects preventive, detective, and recovery controls proportionate to risk.

Boundary: Do not stop at diagrams or generic threat lists. Assign controls and owners, test high-risk abuse cases, plan detection and recovery, and avoid treating low likelihood as harmless when impact is catastrophic.

Read the examples and nearby terms

Keep the rest within reach

Twenty terms cannot cover the working vocabulary of software engineering. Memorizing another two hundred terms in sequence would not create a working vocabulary either. Vocabulary becomes useful when a live problem creates a reason to retrieve it.

The Software Engineering Field Guide is the larger reference. It covers design pressure, code smells, refactoring, patterns, architecture, testing, concurrency, distributed systems, reliability, security, and performance. Search it by a term, an alias, or the symptom you recognize in code. Each result has its own page, so opening Adapter does not make a browser render hundreds of unrelated examples first. Related concepts are links rather than inert lists. Each page also identifies the broader source tradition used for that part of the reference.

The examples put explicit code before compressed syntax. That is partly a teaching choice and partly mercy for my ailing 1990s brain. A dense lambda often becomes ordinary once it is expanded into a named function with visible parameters and a return value. Concision is valuable when it removes noise. It is not valuable when it merely conceals the mechanism from the person trying to learn it.

A language control lets readers choose C#, Python, or both, and preserves that choice from one concept page to the next. The field guide also marks concepts by reading depth. Those labels direct attention without pretending that Refactoring, dependency inversion, and the two meanings of singleton deserve the same priority as a rarely encountered specialist term.

My first program held one fixed string beside one changing value. I did not know enough to call that a design decision. I only knew that the pieces could meet. The vocabulary came later. At its best, it helps me see which pieces may change, which promises must hold, and how to explain the difference to someone else.