Supple Design's 'standalone classes' principle pushes for minimizing a class's dependencies on other concrete types to reduce the mental overhead of understanding it in isolation. At what point does chasing standalone classes across a large domain model start working against you, and how do you recognize that trade-off point in practice?
answer
- standalone = few/no concrete dependencies, reason in isolation
- great fit for value objects like Money, DateRange
- genuinely relational concepts resist standalone-ification
- forced standalone-ness moves coupling to the caller
- watch for ID-based references adding indirection cost
basics
~20 sA class that depends on almost nothing else is easy to understand and test on its own. But if you go too far and strip out every relationship, you can end up hiding real connections between concepts that the domain actually needs, making the bigger picture harder to see.
solid answer
~60 sStandalone classes reduce the 'mental interconnectedness' a reader has to hold to understand one class -- ideally, a class depends only on primitives, a few well-understood value types, and maybe a small number of other standalone types, so you can reason about it without loading half the domain model into your head. This pays off heavily for value objects and low-level building blocks like Money, DateRange, and Address. The trade-off shows up as the model grows: some concepts are genuinely relational, e.g. an Order legitimately needs to know about LineItems and a Customer, and forcing those into standalone form means either passing everything as loosely-typed primitives (losing type safety and intention-revealing interfaces) or introducing indirection layers (IDs instead of references, event-based decoupling) whose own complexity can exceed the coupling they removed. The practical signal to stop is when 'making it standalone' requires the caller to reassemble context the class actually needs to do its job correctly -- at that point you're just moving the coupling to the call site instead of eliminating it.
go deeper
Can explain that a class with fewer dependencies is easier to understand and test, and identify an obvious value-object candidate for making standalone.
Applies standalone design to new value objects and utility-style classes, and recognizes when a class's dependency list is unnecessarily long for what it does.
Judges which domain concepts are genuinely relational versus incidentally coupled, and avoids stripping real relationships into raw primitives just to chase a 'zero dependency' metric.
Sets model-wide guidance on where standalone design pays off versus where it shifts coupling to call sites or introduces costly indirection (ID-based references, decoupling layers), and can justify that trade-off with a concrete example from the codebase's history.
## What standalone means **Standalone classes** means deliberately minimizing how many other concrete types a class depends on, so that understanding, testing, and modifying that one class doesn't require holding a large portion of the rest of the domain model in your head at the same time. Mechanically, this means: - favoring dependencies on primitives and a small number of other already-standalone value types, such as numbers, strings, dates, and things like a `Money` or `DateRange` that are themselves standalone, over dependencies on large entities, services, or repositories; - auditing a class's constructor and method parameters and fields for concrete types that could be removed, narrowed to an interface, or replaced with a value that doesn't require the whole surrounding object graph. ## Why it pays off The motivation is cognitive load and testability. A class with zero or few dependencies can be constructed and exercised in a unit test with no mocks, no fixtures, and no setup beyond the values it directly needs -- you read its interface, build one, and reason about its behavior completely in isolation. This pays off disproportionately for value objects and small computational building blocks, like `Money`, `DateRange`, and `Percentage`, that get reused throughout a codebase: every place that uses them benefits from the same low-friction understanding, and changes to them are easy to verify don't ripple outward because there's little for them to ripple through in the first place. ## The trade-off The trade-off is that not every domain concept is standalone by nature -- some are genuinely relational, meaning their behavior is only meaningful in terms of other concrete types. An `Order` legitimately needs to know about its `LineItems` and its `Customer`; a `LoanApplication` legitimately needs an applicant, a requested amount, and an underwriter to do anything useful. Applying 'minimize dependencies' as an absolute, context-free rule to a genuinely relational entity doesn't remove the coupling those relationships represent, it just relocates it. If a class's dependency is stripped out and replaced with loosely-typed primitives, such as an ID string instead of a typed reference, or three separate booleans instead of a status enum from another type, the information the class needs is still required at every call site, just now without the type safety and self-documentation the original dependency provided. ## How over-application fails - In practice, the failure shows up as a class whose method signature grows longer and more primitive-obsessed after a well-intentioned decoupling pass -- `validate(Customer customer)` becomes `validate(String customerId, boolean isCustomerActive, String customerCountryCode)` -- and now every caller has to independently look up and correctly assemble those three values, duplicating logic that used to live in one place, wherever that `Customer` object was obtained, and losing the guarantee that 'an active customer in Germany' is a coherent, validated concept rather than three coincidentally-consistent primitives. - Another failure mode is over-applying reference-by-ID as a decoupling technique across an entire domain model: every entity refers to its collaborators only by ID, which does reduce compile-time coupling, but every consumer that actually needs the related data now has to perform its own lookup or join, often duplicating that lookup logic across many call sites -- the indirection layer's own complexity can end up exceeding the coupling it removed. ## A good candidate and a poor one - A `DateRange` class holding a start date, end date, and methods like `overlaps(DateRange)` and `durationInDays()` is a strong standalone candidate: it's fully self-contained, trivially testable, and reusable anywhere date-range logic is needed, from a loan's grace period to a hotel booking's stay length, with no changes to `DateRange` itself. - Contrast that with a `LoanApplication`, which needs an applicant, a requested amount, a credit-check result, and an assigned underwriter to make any decision at all; trying to force it toward 'standalone' by replacing those with raw identifiers just means every piece of code operating on a `LoanApplication` has to separately fetch and correctly interpret applicant and underwriter data, reproducing the same coupling without the compiler's help. The practical judgment call -- recognizing `DateRange` as a good standalone candidate and `LoanApplication` as a poor one -- is what separates using this principle well from applying it as blind dogma across an entire model.
- Give a concrete example of a domain type that should stay standalone, and one that shouldn't.DateRange, with a start date, end date, and methods like overlaps() and duration(), should be standalone -- it needs nothing beyond its own two dates to be fully useful and testable in isolation. OrderLine, by contrast, is inherently relational: it doesn't mean anything without knowing which Product and Order it belongs to, so forcing it to be standalone, say by only storing a raw product name string, would just discard type-safe information the domain genuinely needs.
- How does 'standalone classes' interact with dependency injection and testability?Standalone classes are trivially testable without mocks, since there's nothing to fake -- you just construct one and call its methods. As classes gain real dependencies, DI and mocking become the standard way to keep them testable, but that's inherently more test-setup cost than a standalone class, which is exactly why pushing simple concepts, like value objects and calculators, toward standalone form pays off disproportionately.
- What's a warning sign that a team has over-applied standalone classes?If constructing or calling a supposedly 'standalone' class requires the caller to pass in five loosely-typed parameters that used to be a single well-typed dependency, or if related concepts communicate only through raw IDs and every consumer re-implements the same lookup or joining logic, the coupling didn't disappear -- it just moved to every call site and lost its type safety along the way.
Like a Lego brick versus a pre-built car chassis: a single 2x4 brick is standalone -- you can pick it up and understand it with zero context -- but a chassis is inherently relational to the wheels and engine mounts it's built for, and trying to make it 'standalone' by stripping those relationships just means whoever uses it has to somehow remember which wheels fit, off to the side.
saying these in an interview costs you the question
- treating 'standalone' as an absolute rule applied to every class regardless of domain shape
- stripping a genuinely relational entity down to raw IDs/primitives and calling it 'decoupled'
- forcing every dependency out of a class only to have every caller reconstruct the same context manually
- claiming standalone classes eliminate the need for dependency injection or testing infrastructure across the whole codebase
- not distinguishing value objects (good standalone candidates) from entities with real relationships