Several classes in one application had almost the same shape. Each extended a connection manager, read named configuration, and delegated object creation to a factory. One managed payment gateways, another rating clients, and another booking clients.
The shared abstraction made their code look alike. Their callers were asking different questions.
Payment callers needed a gateway with a given name. Rating callers sometimes needed to decide which connection matched a shipment. Booking callers selected connections from different rules for creating, collecting, and cancelling, then assembled operation-specific services around the result.
A retained refactor removed the manager package. Lookup became a registry.
Use-case policy became selectors. Valid assembly became factories. The diff
does not prove fewer bugs or faster development, and some ambient context
remained. It does show why Manager was an incomplete description: the name
hid decisions which changed for different reasons.
Start with the sentence a caller can say
A role is easier to find from a caller’s verb than from the contents of a class.
Compare these requests:
give me the adapter named "express"
choose an adapter for this booking
build a booking operation with that adapter
execute the bookingThey describe four different results:
| Caller verb | Result | Plausible role |
|---|---|---|
| find by name | configured capability | registry |
| choose from facts | selected name or policy result | selector |
| build valid object | ready operation | factory |
| perform operation | business outcome | service or command |
Manager could mean any combination of them. So could Helper or Service.
The problem is not that these words are forbidden. The problem is that they do
not tell a caller which promise is safe to depend on.
Try completing one sentence:
I need this object because it ___.
If five callers fill the blank with unrelated verbs, the class may be hiding several roles. If they all say “coordinates the recovery of one payment,” then a broad manager may be exactly right.
Similar mechanics do not create the same role
The old managers shared useful mechanics:
read connection configuration
choose a named entry
create the configured implementation
cache or reconnect itThat resemblance justified reusable infrastructure. It did not mean every business caller should depend on the entire connection-manager API.
One retained replacement reduced a payment boundary to a single operation: resolve the gateway for this explicit name. The caller did not need to change the default connection, disconnect all connections, or extend the manager at runtime.
Another path had two independent questions:
which rating connection fits this request?
which configured client has that name?The first question depended on business input. The second depended on
configuration. Putting both behind manager() made their ownership invisible.
A selector and registry made the difference inspectable.
The booking path exposed a third question: after selecting a connection, how should a valid operation be assembled? Shipment creation, pickup, and cancellation required different input and collaborators. The refactor gave each selection policy and construction path a specific home.
This was more than replacing one noun with another. It separated mechanisms which had different reasons to change.
A rename changes vocabulary, not responsibility
Renaming ClientManager to ClientRegistry can improve a search result while
leaving the class responsible for:
- choosing an implementation from business data;
- reading framework configuration;
- constructing the implementation;
- caching mutable connections;
- assembling an operation; and
- executing it.
That is still the same dependency surface.
The public reproduction makes this distinction explicit. Its synthetic manager owns lookup, selection, and construction. Renaming it to registry leaves all three change paths attached to one object. Splitting it into registry, selector, and factory gives each declared change type a different owner.
The fixture is intentionally modest. It does not calculate cohesion or prove that three objects are universally better than one. It verifies only that a rename cannot isolate a change pressure which the implementation still mixes.
Use a change table before choosing names:
| Proposed change | Decision affected | Current owner |
|---|---|---|
| add configured adapter | named lookup | manager |
| route one country differently | use-case selection | manager |
| require a new constructor value | valid assembly | manager |
If the table keeps naming the same broad class for unrelated decisions, a sharper noun will not fix it.
A registry should not decide why you need an entry
A registry answers a deliberately boring question:
name -> configured capabilityIt can validate that the name exists, construct or retrieve the matching implementation, and report a useful error when configuration is invalid. It should not need a shipment, customer, locale, or command just to perform lookup.
Those facts belong to selection:
booking facts -> connection nameKeeping selection pure has practical benefits. Its cases can be read without network clients or framework configuration. A test can supply business facts and inspect the chosen name. Changing a routing rule need not recreate every adapter.
The registry can then be tested with a different contract:
known name -> correct capability
missing name -> explicit configuration errorNeither object needs to know how the other reaches its result.
This separation is not mandatory when selection is truly identical to lookup. If every caller already has the exact configured name, introducing a selector which merely returns its argument is ceremony. Roles should follow decisions which exist, not patterns which might be useful someday.
A factory owns a usable result
Selection answers which capability to use. It does not necessarily produce the operation a caller needs.
A factory can combine:
- validated operation data;
- the selected capability;
- metadata or persistence collaborators;
- execution mode;
- raw input required at one boundary; and
- an explicit policy result.
Its promise is not “I create objects” in the abstract. It is “I return this operation ready to use.”
That promise also reveals incomplete extraction. In the retained refactor, one factory still consulted ambient tenant and request context. Moving code out of the service made the construction path visible, but it did not make every input explicit.
That is an important evidence boundary. A class named Factory is not proof
that construction is deterministic. Inspect its parameters and every value it
pulls from the container or process. Naming can expose the next problem without
already solving it.
Keep a broad object when coordination is the role
Not every object should collapse into one-method fragments.
A process manager can legitimately coordinate a long-running outcome. A unit of work can own atomic persistence across several repositories. A booking operation can validate, call a provider, persist the result, and report an outcome when those steps form one failure-sensitive transition.
The distinction is whether the sequence protects one invariant or merely collects convenient functions.
Ask three questions before splitting:
- Do the responsibilities change for different reasons?
- Can a caller depend on one without gaining access to the others?
- Does separating them make a real decision visible?
If all responsibilities must change together to preserve one outcome, the broad role may be honest. If the proposed pieces only forward calls to each other, the split adds navigation without adding a boundary.
Names such as Coordinator and ProcessManager are still broad, but they can
be precise when the coordinated process is named and its outcome is testable.
ShipmentRecoveryCoordinator communicates more than ShipmentManager
because it tells the reader why the object exists.
Tests should use the role’s language
Tests reveal whether a name is honest.
A registry test should discuss names, configured implementations, and missing entries. A selector test should discuss inputs, precedence, and chosen policy. A factory test should discuss complete construction and required collaborators. An operation test should discuss the resulting business state.
When every test for Manager needs a different vocabulary, the test suite is
showing the same ambiguity as the callers.
Do not measure success by class count or average method count. After a refactor, verify the behavior which mattered before it:
- configured connections still resolve;
- selection precedence is unchanged or deliberately revised;
- missing configuration still fails clearly;
- constructed operations receive every required collaborator; and
- callers cannot accidentally reach unrelated lifecycle controls.
The retained refactor had focused coverage for registries and selectors and updated callers to receive their dependencies. It did not retain a measured before-and-after maintenance result. The defensible outcome is therefore narrow: previously hidden roles became explicit and testable.
A class name is a promise to its callers. Make it name the capability they use
or the decision it owns. When the only honest word is Manager, finish the
sentence: manager of which outcome, for whom, and under what invariant?