I used to reach for inheritance whenever I wanted reuse. Need logging? Extend the base class. Need “one more feature”? Subclass it. It feels like progress because you write less code today.\n\nThen the bill shows up: surprising behavior changes after a refactor, subclasses that break when a superclass evolves, tests that become glued to implementation details, and “simple” hierarchies that turn into a web of overrides you’re afraid to touch.\n\nWhen I want code that survives real product change—new pricing rules, new auth flows, new transports, new persistence choices—I default to composition. I build objects out of smaller objects and delegate work to them. That gives me smaller units to test, less coupling to unstable details, and the ability to swap behavior without tearing up a class tree.\n\nBy the end of this post, you’ll be able to:\n\n- Spot when inheritance is quietly creating risk (even when it compiles and passes tests).\n- Refactor common inheritance patterns into composition with clean delegation.\n- Design “plug-in” style behavior using interfaces, decorators, and small policy objects.\n- Decide when inheritance is still the right move—and how to keep it safe.\n\n## Inheritance feels cheap until it isn’t\nInheritance models an “is-a” relationship: a subtype promises it can stand in for its supertype without surprising callers. That promise is stronger than many people realize.\n\nWhen you extend a class, you inherit:\n\n- Its public API (including methods you never wanted to expose).\n- Its internal assumptions (often undocumented).\n- Its future changes (even if you didn’t ask for them).\n\nTwo problems show up repeatedly in production code.\n\n### 1) The fragile base class problem\nA superclass can change in ways that are reasonable for the superclass but disastrous for subclasses. Even a harmless internal refactor—rewiring one method to call another—can break a subclass that overrides one of those methods.\n\nThis shows up as “my subclass worked for years and now it fails after an unrelated library update.” The scary part is that nothing in your code changed; your dependency did. With deep inheritance, your objects are more like extensions of someone else’s implementation than independent units with clear contracts.\n\n### 2) You inherit more surface area than you can defend\nSubclassing a rich type (collections, framework base classes, “manager” classes) means you implicitly accept responsibilities for behavior you didn’t design. You might only want “a list with auditing,” but now you’re responsible for dozens of list operations and their corner cases.\n\nAlso: you expose things you didn’t intend to expose. A subclass has all public/protected methods of the parent. If you were trying to build a safe abstraction, inheritance often punches a big hole in it by accident.\n\nInheritance can still be correct, but I treat it as a strong coupling mechanism. If I’m not willing to lock two classes together for the long haul, I avoid it.\n\n## Composition: building with “has-a” and delegation\nComposition models a “has-a” relationship: instead of becoming a thing, your class holds a thing (or several things) and forwards work to them.\n\nThat sounds simple, but it changes how you design:\n\n- You model behavior as replaceable collaborators (policies, strategies, adapters).\n- You hide internals behind narrow interfaces.\n- You can change parts without rewriting the whole.\n\nA useful analogy: inheritance is like welding two metal parts together—strong, permanent, and hard to undo. Composition is like bolting parts together—still strong, but you can replace one piece without melting everything down.\n\nIn modern Java codebases (DI containers, modular services, heavy testing), composition fits naturally because you already pass dependencies in constructors and you already mock or fake collaborators.\n\nOne subtle but important point: composition forces you to think in terms of contracts. If you wrap a collaborator behind an interface you own, you’re no longer depending on a concrete class’s internal behavior. You’re depending on a stable boundary you can control.\n\n## Example 1: “Audited list” — where inheritance creates a bug you won’t expect\nA classic trap is subclassing a collection to “add one behavior.” I’ve seen teams extend ArrayList or HashMap for metrics, tracing, access control, caching, or auditing.\n\nThe issue: the base class may implement one method in terms of another method, but not always in the way your override assumes.\n\nHere’s a runnable example that looks correct at first glance.\n\n import java.util.ArrayList;\n import java.util.List;\n\n public class InheritanceTrapDemo {\n\n // Intention: count every element that gets added.\n static class AuditedArrayList extends ArrayList {\n private int adds = 0;\n\n @Override\n public boolean add(E e) {\n adds++;\n return super.add(e);\n }\n\n @Override\n public void add(int index, E element) {\n adds++;\n super.add(index, element);\n }\n\n // Looks like it should work, right?\n public int getAdds() {\n return adds;\n }\n }\n\n public static void main(String[] args) {\n AuditedArrayList names = new AuditedArrayList();\n\n names.add("Asha");\n names.add("Mateo");\n\n // Surprise: addAll may bypass our add(E) accounting depending on implementation details.\n List more = List.of("Noor", "Kei", "Lina");\n names.addAll(more);\n\n System.out.println("size=" + names.size());\n System.out.println("adds=" + names.getAdds());\n }\n }\n\nWhat do you expect? Many developers expect adds == 5.\n\nWhat can happen? adds might be 2 (or another incorrect number), because ArrayList#addAll can bulk-copy elements into its backing array without calling your overridden add methods.\n\nThis is not “Java being weird.” It’s inheritance exposing you to implementation choices in a class you do not control.\n\n### Composition fix: wrap a List and delegate explicitly\nInstead of extending ArrayList, I wrap a List and count adds in the wrapper.\n\n import java.util.ArrayList;\n import java.util.Collection;\n import java.util.Iterator;\n import java.util.List;\n import java.util.ListIterator;\n\n public class CompositionAuditedListDemo {\n\n static final class AuditedList implements List {\n private final List delegate;\n private int adds;\n\n public AuditedList(List delegate) {\n this.delegate = delegate;\n }\n\n public int getAdds() {\n return adds;\n }\n\n @Override\n public boolean add(E e) {\n adds++;\n return delegate.add(e);\n }\n\n @Override\n public void add(int index, E element) {\n adds++;\n delegate.add(index, element);\n }\n\n @Override\n public boolean addAll(Collection c) {\n adds += c.size();\n return delegate.addAll(c);\n }\n\n @Override\n public boolean addAll(int index, Collection c) {\n adds += c.size();\n return delegate.addAll(index, c);\n }\n\n // — Everything else delegates without changing semantics —\n\n @Override public int size() { return delegate.size(); }\n @Override public boolean isEmpty() { return delegate.isEmpty(); }\n @Override public boolean contains(Object o) { return delegate.contains(o); }\n @Override public Iterator iterator() { return delegate.iterator(); }\n @Override public Object[] toArray() { return delegate.toArray(); }\n @Override public T[] toArray(T[] a) { return delegate.toArray(a); }\n @Override public boolean remove(Object o) { return delegate.remove(o); }\n @Override public boolean containsAll(Collection c) { return delegate.containsAll(c); }\n @Override public boolean removeAll(Collection c) { return delegate.removeAll(c); }\n @Override public boolean retainAll(Collection c) { return delegate.retainAll(c); }\n @Override public void clear() { delegate.clear(); }\n @Override public E get(int index) { return delegate.get(index); }\n @Override public E set(int index, E element) { return delegate.set(index, element); }\n @Override public E remove(int index) { return delegate.remove(index); }\n @Override public int indexOf(Object o) { return delegate.indexOf(o); }\n @Override public int lastIndexOf(Object o) { return delegate.lastIndexOf(o); }\n @Override public ListIterator listIterator() { return delegate.listIterator(); }\n @Override public ListIterator listIterator(int index) { return delegate.listIterator(index); }\n @Override public List subList(int fromIndex, int toIndex) { return delegate.subList(fromIndex, toIndex); }\n }\n\n public static void main(String[] args) {\n AuditedList names = new AuditedList(new ArrayList());\n names.add("Asha");\n names.add("Mateo");\n names.addAll(List.of("Noor", "Kei", "Lina"));\n\n System.out.println("size=" + names.size());\n System.out.println("adds=" + names.getAdds());\n }\n }\n\nThis wrapper is boring—and that’s the point. The semantics are now yours. You count what you claim to count, regardless of how ArrayList chooses to implement bulk operations.\n\nIf you don’t want to forward a big interface, you can also wrap only what you need (for example, expose a narrower NameRegistry interface), which often produces better design.\n\n### Edge cases for wrappers (so you don’t trade one problem for another)\nWhen I see teams adopt composition for collections, the first version is often correct but incomplete. Here are the edge cases I now check deliberately:\n\n- equals/hashCode: if your wrapper will be used as a key in maps or compared for equality, decide whether equality should be based on identity, on the delegate, or on wrapper-specific state (like adds). Make it explicit.\n- subList: returning delegate.subList(...) leaks the underlying list and bypasses auditing. If that matters, return a wrapped subList instead.\n- Iterators: iterator() can leak mutation operations (remove) that you might want to audit. If you care, return a wrapping iterator.\n- Serialization: if you need serialization, prefer designing an explicit DTO rather than relying on whatever your delegate does.\n- Thread safety: wrapping does not automatically make something thread safe. If you need concurrency, compose with a lock or a concurrent delegate and make your invariants explicit.\n\nComposition gives you control, but you have to choose what you want to control. That’s a feature, not a bug.\n\n## A quick decision table: inheritance vs composition\nWhen I’m choosing quickly, I use a rule of thumb: inheritance is for types, composition is for behavior.\n\n
Inheritance tends to do poorly
\n
—
\n
Hard (type is fixed)
\n
Hard (inherit large surface)
\n
Risky (fragile coupling)
\n
Awkward (single inheritance)
\n
Easy to lie accidentally
\n\nIf I can’t confidently say “this subtype must behave exactly like its supertype,” I stop and switch to composition.\n\n### A fast smell checklist I actually use\nIf I’m reviewing a PR and I see a new subclass, I ask a few quick questions:\n\n- Did we subclass just to reuse code, or because the subtype really is substitutable everywhere the supertype is used?\n- Are we overriding methods that the base class calls internally (a common fragile-base setup)?\n- Are we inheriting a huge API surface that we don’t plan to test exhaustively?\n- Are there already multiple “feature” subclasses that will need to be combined eventually?\n- Would an interface + delegation make the dependency boundary clearer?\n\nWhen two or more answers make me uneasy, composition almost always wins.\n\n## Example 2: Pricing rules that change every quarter (composition shines here)\nPricing is a real-world hotspot because business rules evolve. When teams use inheritance, it often turns into a class tree like:\n\n- BasePriceCalculator\n- HolidayPriceCalculator extends BasePriceCalculator\n- HolidayPlusLoyaltyCalculator extends HolidayPriceCalculator\n- HolidayPlusLoyaltyPlusBulkCalculator extends HolidayPlusLoyaltyCalculator\n\nThat’s not “object-oriented elegance.” It’s a brittle encoding of combinations.\n\nWith composition, I model each rule as a small policy and assemble them.\n\n### Runnable example: pipeline of adjustments\n\n import java.math.BigDecimal;\n import java.math.RoundingMode;\n import java.time.DayOfWeek;\n import java.time.LocalDate;\n import java.util.List;\n\n public class CompositionPricingDemo {\n\n record Money(BigDecimal amount, String currency) {\n Money {\n if (amount == null
d == DayOfWeek.SUNDAY);\n if (!weekend) return current;\n\n BigDecimal multiplier = BigDecimal.ONE.subtract(percentOff);\n return current.times(multiplier);\n }\n\n @Override\n public String name() {\n return "WeekendSale(" + percentOff + ")";\n }\n }\n\n static final class LoyaltyDiscount implements PriceAdjustment {\n private final BigDecimal percentOff;\n\n LoyaltyDiscount(BigDecimal percentOff) {\n this.percentOff = percentOff;\n }\n\n @Override\n public Money apply(Money current, Cart cart, LocalDate today) {\n if (!cart.loyaltyMember()) return current;\n return current.times(BigDecimal.ONE.subtract(percentOff));\n }\n\n @Override\n public String name() {\n return "LoyaltyDiscount(" + percentOff + ")";\n }\n }\n\n static final class BulkFeeWaiver implements PriceAdjustment {\n private final int minItems;\n private final Money fee;\n\n BulkFeeWaiver(int minItems, Money fee) {\n this.minItems = minItems;\n this.fee = fee;\n }\n\n @Override\n public Money apply(Money current, Cart cart, LocalDate today) {\n if (cart.items() < minItems) return current;\n return current.minus(fee);\n }\n\n @Override\n public String name() {\n return "BulkFeeWaiver(minItems=" + minItems + ")";\n }\n }\n\n static final class PriceCalculator {\n private final List adjustments;\n\n PriceCalculator(List adjustments) {\n this.adjustments = adjustments;\n }\n\n Money calculate(Money base, Cart cart, LocalDate today) {\n Money current = base;\n for (PriceAdjustment a : adjustments) {\n current = a.apply(current, cart, today);\n }\n return current.rounded();\n }\n }\n\n public static void main(String[] args) {\n Money base = new Money(new BigDecimal("120.00"), "USD");\n Cart cart = new Cart(12, true);\n\n Money shippingFee = new Money(new BigDecimal("7.50"), "USD");\n\n PriceCalculator calculator = new PriceCalculator(List.of(\n new WeekendSale(new BigDecimal("0.10")),\n new LoyaltyDiscount(new BigDecimal("0.05")),\n new BulkFeeWaiver(10, shippingFee)\n ));\n\n LocalDate saturday = LocalDate.of(2026, 2, 7); // a Saturday\n System.out.println("Final: " + calculator.calculate(base, cart, saturday));\n }\n }\n\nWhy this stays healthy as requirements grow:\n\n- You add a new rule by adding a class that implements PriceAdjustment.\n- You can A/B test ordering and combinations by changing the list, not the type hierarchy.\n- You can unit test each adjustment in isolation with tiny test fixtures.\n\nIf you’ve ever had to unwind a giant subclass chain for pricing, promotions, or authorization, this “pipeline of small policy objects” feels like fresh air: the combinations are data, not types.\n\n### Make it more production-friendly: tracing, explainability, and rule ordering\nIn real systems, pricing often needs to be explainable (“Why did the price change?”), observable (“Which rule dominates?”), and safe (“Don’t apply two discounts that shouldn’t stack”). Composition makes those concerns easier because the pipeline is explicit.\n\nHere’s a small extension: keep a receipt of each adjustment and guard ordering with a rule set.\n\n import java.util.ArrayList;\n import java.util.List;\n import java.util.Objects;\n\n record PriceLine(String rule, Money before, Money after) {}\n\n record PriceResult(Money finalPrice, List lines) {}\n\n static final class AuditedPriceCalculator {\n private final List adjustments;\n\n AuditedPriceCalculator(List adjustments) {\n this.adjustments = List.copyOf(adjustments);\n }\n\n PriceResult calculateWithBreakdown(Money base, Cart cart, java.time.LocalDate today) {\n Objects.requireNonNull(base);\n List lines = new ArrayList();\n Money current = base;\n\n for (PriceAdjustment a : adjustments) {\n Money next = a.apply(current, cart, today);\n if (next == null) throw new IllegalStateException(a.name() + " returned null");\n if (!next.currency().equals(current.currency())) {\n throw new IllegalStateException(a.name() + " changed currency");\n }\n if (next.amount().compareTo(java.math.BigDecimal.ZERO) < 0) {\n throw new IllegalStateException(a.name() + " produced negative price");\n }\n lines.add(new PriceLine(a.name(), current, next));\n current = next;\n }\n\n Money rounded = current.rounded();\n if (!rounded.equals(current)) {\n lines.add(new PriceLine("Rounding", current, rounded));\n }\n return new PriceResult(rounded, List.copyOf(lines));\n }\n }\n\nI like this pattern because it turns pricing into something you can debug in production without guessing which override ran. It also sets you up for “rules as configuration” later if you go that direction.\n\n### Testing advantage: tiny tests, less mocking\nWith inheritance-heavy pricing, tests often instantiate a deep subclass and assert a final number—brittle and hard to pinpoint when it fails. With composition, I can test each adjustment with a 5-line test fixture.\n\nFor example, a conceptual JUnit test for WeekendSale can be as small as:\n\n // Pseudocode-ish JUnit style\n // assertEquals(new Money(108.00, "USD"), weekendSale.apply(new Money(120, "USD"), cart, saturday));\n\nEven without writing the tests here, the point is the shape of the code: small, isolated collaborators naturally lead to small, isolated tests.\n\n## Example 3: “Add logging” without subclass explosions (Decorator via composition)\nLogging is one of the most common reasons I see people reach for inheritance: “I want everything this service does, plus logs.” That instinct is understandable—and it’s exactly where composition shines.\n\nLet’s say I have a payment interface:\n\n import java.time.Instant;\n\n record PaymentRequest(String userId, String orderId, long cents) {}\n record PaymentReceipt(String paymentId, Instant processedAt) {}\n\n interface PaymentProcessor {\n PaymentReceipt charge(PaymentRequest request);\n }\n\nA straightforward implementation might call an external gateway. Now I want logging, timing, and maybe retry. If I do that with inheritance, I tend to end up with base classes like BasePaymentProcessor and a tangle of overrides, or a chain of subclasses where order matters.\n\nWith composition, I wrap the interface. This is the decorator pattern in its simplest form: a class implements an interface and delegates to another implementation of the same interface.\n\n import java.time.Duration;\n import java.time.Instant;\n import java.util.Objects;\n\n final class LoggingPaymentProcessor implements PaymentProcessor {\n private final PaymentProcessor delegate;\n\n LoggingPaymentProcessor(PaymentProcessor delegate) {\n this.delegate = Objects.requireNonNull(delegate);\n }\n\n @Override\n public PaymentReceipt charge(PaymentRequest request) {\n Instant start = Instant.now();\n System.out.println("charge.start user=" + request.userId() + " order=" + request.orderId());\n try {\n PaymentReceipt receipt = delegate.charge(request);\n Duration d = Duration.between(start, Instant.now());\n System.out.println("charge.ok paymentId=" + receipt.paymentId() + " ms=" + d.toMillis());\n return receipt;\n } catch (RuntimeException e) {\n Duration d = Duration.between(start, Instant.now());\n System.out.println("charge.fail ms=" + d.toMillis() + " err=" + e.getClass().getSimpleName());\n throw e;\n }\n }\n }\n\nNow I can stack behaviors:\n\n PaymentProcessor core = new GatewayPaymentProcessor(…);\n PaymentProcessor withLogging = new LoggingPaymentProcessor(core);\n PaymentProcessor withRetry = new RetryingPaymentProcessor(withLogging, …);\n PaymentProcessor withRateLimit = new RateLimitedPaymentProcessor(withRetry, …);\n\nThe important practical win: I can change the order without rewriting types. That’s incredibly useful for cross-cutting concerns where ordering changes semantics (retry before logging vs logging each attempt, etc.).\n\n### Pitfall: don’t leak the delegate\nA common anti-pattern is adding a getDelegate() accessor “for convenience.” That convenience usually becomes a bypass, and suddenly half the system reaches around your decorator. If the whole point is to enforce behavior (logging, auth, metrics), exposing the delegate undermines it.\n\nWhen I need inspection for testing or debugging, I prefer:\n\n- Keeping the field private and final\n- Using a type-based DI graph inspection in tests\n- Or adding explicit introspection methods that do not allow bypassing behavior\n\n## Example 4: Authorization rules as policies (composition beats subclassing controllers)\nAuthorization is another hotspot where inheritance looks tempting: “AdminController extends BaseController” or “SecuredService extends Service.” The problem is that authorization is rarely a single axis. It depends on user role, resource ownership, feature flags, request context, and sometimes product tier.\n\nInstead of encoding auth in base classes, I compose services with a policy object.\n\n import java.util.Set;\n\n record User(String id, Set roles) {}\n record Document(String id, String ownerId) {}\n\n interface DocumentPolicy {\n boolean canRead(User user, Document doc);\n boolean canEdit(User user, Document doc);\n }\n\n final class DefaultDocumentPolicy implements DocumentPolicy {\n @Override\n public boolean canRead(User user, Document doc) {\n if (user.roles().contains("ADMIN")) return true;\n return doc.ownerId().equals(user.id());\n }\n\n @Override\n public boolean canEdit(User user, Document doc) {\n if (user.roles().contains("ADMIN")) return true;\n return doc.ownerId().equals(user.id()) && user.roles().contains("EDITOR");\n }\n }\n\n interface DocumentRepository {\n Document getById(String id);\n void save(Document doc);\n }\n\n final class DocumentService {\n private final DocumentRepository repo;\n private final DocumentPolicy policy;\n\n DocumentService(DocumentRepository repo, DocumentPolicy policy) {\n this.repo = repo;\n this.policy = policy;\n }\n\n public Document read(User user, String docId) {\n Document doc = repo.getById(docId);\n if (!policy.canRead(user, doc)) throw new SecurityException("forbidden");\n return doc;\n }\n\n public void edit(User user, Document updated) {\n Document existing = repo.getById(updated.id());\n if (!policy.canEdit(user, existing)) throw new SecurityException("forbidden");\n repo.save(updated);\n }\n }\n\nNow “different auth flows” become different policy implementations, not different service subclasses. For example:\n\n- TieredDocumentPolicy for paid tiers\n- FeatureFlagDocumentPolicy for a gradual rollout\n- RegionalCompliancePolicy for special jurisdictions\n\nAnd because policies are small and pure, they’re easy to unit test and reason about.\n\n### Combining policies safely\nSometimes you need to combine policies: “must satisfy both” or “satisfy any.” Composition makes that easy too.\n\n import java.util.List;\n\n final class AllOfDocumentPolicy implements DocumentPolicy {\n private final List policies;\n\n AllOfDocumentPolicy(List policies) {\n this.policies = List.copyOf(policies);\n }\n\n @Override\n public boolean canRead(User user, Document doc) {\n for (DocumentPolicy p : policies) {\n if (!p.canRead(user, doc)) return false;\n }\n return true;\n }\n\n @Override\n public boolean canEdit(User user, Document doc) {\n for (DocumentPolicy p : policies) {\n if (!p.canEdit(user, doc)) return false;\n }\n return true;\n }\n }\n\nIf I did this with inheritance, I’d need a combinatorial explosion of subclasses (“AdminOrOwnerAndEditorInTier2…”). With composition, it’s just list wiring.\n\n## Example 5: Transport and persistence choices (composition keeps boundaries clean)\nI’ll give you a scenario that shows up when products evolve: the code started as a monolith that used a database directly. Later you add a cache. Then you add an HTTP client to call a remote service. Then you add a queue. Then you add a new persistence option for a region.\n\nIf your core service inherits from a base class that “knows” about a particular storage or transport, you end up with deep, hard-to-test hierarchies.\n\nWith composition, I push those choices behind interfaces that represent what my core logic actually needs.\n\n record UserProfile(String userId, String displayName) {}\n\n interface UserProfiles {\n UserProfile get(String userId);\n void put(UserProfile profile);\n }\n\nNow I can implement it in different ways:\n\n- DbUserProfiles uses a database\n- HttpUserProfiles calls another service\n- CachedUserProfiles wraps another UserProfiles and adds caching\n\n import java.util.Map;\n import java.util.Objects;\n import java.util.concurrent.ConcurrentHashMap;\n\n final class CachedUserProfiles implements UserProfiles {\n private final UserProfiles delegate;\n private final Map cache = new ConcurrentHashMap();\n\n CachedUserProfiles(UserProfiles delegate) {\n this.delegate = Objects.requireNonNull(delegate);\n }\n\n @Override\n public UserProfile get(String userId) {\n return cache.computeIfAbsent(userId, delegate::get);\n }\n\n @Override\n public void put(UserProfile profile) {\n delegate.put(profile);\n cache.put(profile.userId(), profile);\n }\n }\n\nThe high-level service never changes its type because storage and transport are “details.” I swap implementations based on environment, region, rollout stage, or test setup.\n\nThis is one of the biggest practical payoffs of composition: it keeps your domain logic stable while infrastructure churn happens around it.\n\n## Refactoring inheritance into composition (a step-by-step recipe)\nWhen I say “favor composition,” I’m not saying “rewrite everything.” Most of the time I’m refactoring incrementally. Here’s the path that’s worked for me repeatedly.\n\n### Step 1: Identify what you actually wanted\nIf a subclass exists, ask: what did we add?\n\n- Was it a cross-cutting concern (logging, metrics, caching, retries)?\n- Was it a rule/policy (discount, permission, validation)?\n- Was it a new data source or transport?\n- Was it just code reuse (“I needed three helper methods”)?\n\nThis matters because the right composition shape depends on what the behavior is.\n\n### Step 2: Extract a narrow interface you own\nInstead of inheriting a giant concrete type, define an interface that represents the behavior you need. Make it small. Example: don’t expose List; expose NameRegistry with addName and listNames.\n\nI try to keep the interface at the “domain level,” not the “framework level.” Domain interfaces tend to remain stable longer.\n\n### Step 3: Introduce a delegating implementation\nCreate a class that implements the interface and delegates to the existing concrete implementation. At this stage, behavior is unchanged—this is just a seam.\n\n### Step 4: Move the behavior into decorators/policies\nTake the subclass-only logic and move it into a wrapper that also implements the interface.\n\nIf you need multiple features, create multiple wrappers and compose them in wiring code rather than in types.\n\n### Step 5: Delete the subclass\nOnce call sites depend on the interface and use composed implementations, delete the inheritance.\n\n### Step 6: Add tests at the seam\nThe seam (the interface boundary) is a great place for tests because it’s stable. You can unit test each policy/decorator and write a small integration test for the wiring.\n\n## Common pitfalls when adopting composition (and how I avoid them)\nComposition isn’t magic. You can still build a mess—just a different-shaped mess. Here are the mistakes I see most, and what I do instead.\n\n### Pitfall 1: “Wrapper explosion” without a plan\nIf every little behavior becomes its own wrapper and you stack ten of them everywhere, code can get hard to follow.\n\nWhat I do:\n\n- Keep the core interface small. Smaller interface = smaller wrappers.\n- Group behaviors that change together (one wrapper per concern cluster).\n- Centralize assembly in one place (a factory or DI module) so call sites don’t manually stack wrappers.\n\n### Pitfall 2: Leaking internal types through the interface\nIf your interface returns framework types, you end up coupled anyway. Example: returning HttpServletRequest from a domain service means you didn’t really create a boundary.\n\nWhat I do:\n\n- Translate at the boundary: controller maps request -> domain command, service returns domain result -> controller maps to response.\n- Prefer simple DTOs/records in the domain layer.\n\n### Pitfall 3: Delegation bugs (missing methods, wrong semantics)\nWhen wrapping large interfaces (like List), it’s easy to forget a method or implement it incorrectly.\n\nWhat I do:\n\n- Avoid wrapping huge interfaces unless I truly need them.\n- Prefer narrower interfaces.\n- If I must wrap a large interface, I add targeted tests for any method that can mutate state or bypass invariants (addAll, subList, iterators).\n\n### Pitfall 4: Identity vs equality confusion\nA wrapper might “look like” the underlying object but not behave like it for equality. That can cause subtle bugs in collections.\n\nWhat I do:\n\n- Decide on equality intentionally and document it.\n- If the wrapper adds state (like audit counts), I usually keep identity-based equality to avoid weirdness.\n\n## Performance considerations (composition is usually fine)\nOne objection I hear is: “Isn’t composition slower?” In hot paths, extra indirection and allocations can matter. But in most business applications, the heavy cost is I/O: databases, network calls, serialization, disk, locks. Compared to that, a few extra method calls are typically noise.\n\nHere’s how I think about it in practice (using ranges rather than pretending I can predict your exact environment):\n\n- Additional virtual calls: can be effectively free after JIT inlining in many cases, especially with final classes and stable call sites.\n- Additional allocations (wrappers): can add some overhead, but often the wrappers are long-lived singletons created once (for example, at startup via DI).\n- High-throughput low-latency loops: if you’re writing a tight in-memory loop that runs millions of times per second, measure. You can still use composition, but you may consolidate wrappers or use strategy objects that are passed in rather than layered deeply.\n\nMy rule: default to composition for design. If profiling shows it’s a bottleneck, optimize the specific bottleneck. Don’t pre-pay complexity with inheritance “just in case performance.”\n\n## When inheritance is still the right move (and how I keep it safe)\n“Favor composition over inheritance” is a default, not a law. I still use inheritance when it’s actually modeling a type relationship and the base class was designed for extension.\n\nHere are cases where I’m comfortable inheriting:\n\n### 1) Small, stable, purpose-built base classes\nIf I own the base class and it’s intentionally built for extension (with documented hooks and invariants), inheritance can be clean.\n\n### 2) Sealed hierarchies for algebraic modeling\nIf I’m modeling a closed set of variants (and I can keep it closed), a sealed type hierarchy can be appropriate because it makes illegal states unrepresentable. In those cases, inheritance is used for modeling, not for reuse.\n\n### 3) Framework-required extension points (sparingly)\nSometimes frameworks require inheritance (certain test base classes, UI components, etc.). When that happens, I try to keep the subclass thin and delegate real logic into composed services that I own.\n\n### How I keep inheritance safe\nIf I do inherit, I treat it as a design contract and I defend it:\n\n- I avoid overriding methods that the base class calls internally unless that pattern is explicitly documented as safe.\n- I prefer final methods in the base class and protected “hook” methods with narrow responsibilities.\n- I document invariants and expectations (“subclasses must call super”, “subclasses must be side-effect free”).\n- I keep base classes small. A giant base class is a warning sign.\n\nA simple litmus test: if I can’t write down a crisp contract for subclass authors, I shouldn’t be authoring a base class meant for inheritance.\n\n## Composition patterns I reach for most in Java\nIf you want to build a composition-first toolbox, these patterns cover a lot of ground.\n\n### Strategy (policy objects)\nUse when you have a decision that varies: pricing rules, auth rules, routing rules, validation rules.\n\nShape: interface Policy { ... } + multiple implementations + injected into a service.\n\n### Decorator (wrappers)\nUse when you want to add cross-cutting behavior: logging, metrics, caching, retries, rate limiting, circuit breaking.\n\nShape: class X implements Service { private final Service delegate; ... }\n\n### Adapter\nUse when you want to hide a third-party API or swap implementations later: storage, external clients, messaging, payment gateways.\n\nShape: interface MyPort + class ThirdPartyAdapter implements MyPort\n\n### Composite\nUse when you want to combine multiple implementations: “all of these validators,” “any of these policies,” “first match wins.”\n\nShape: class CompositeX implements X { private final List xs; ... }\n\nThese are all composition-first moves. They keep your type system describing your domain instead of describing your reuse hacks.\n\n## A practical guide: choosing composition in day-to-day code reviews\nWhen I’m reviewing code, I’m not trying to ban inheritance; I’m trying to reduce future risk. Here’s the mindset I apply:\n\n- If the change introduces a new subclass, I ask what contract the subtype is promising and whether we can defend it with tests.\n- If the subclass exists for cross-cutting concerns, I nudge toward decorators.\n- If the subclass exists for “rules,” I nudge toward strategies/policies.\n- If the subclass exists for “integration details,” I nudge toward adapters.\n- If the subclass exists for reuse-only, I nudge toward extraction (helper objects, composition, or simply duplication if the shared logic is tiny and unstable).\n\nSometimes the best short-term move is even simpler: copy the 20 lines and keep it local. If the code is not stable yet, forcing reuse via inheritance can lock you into the wrong abstraction prematurely. Composition makes reuse more deliberate; sometimes “no reuse yet” is the deliberate choice.\n\n## Closing thought\nInheritance is powerful, but it’s also a long-term commitment: you’re linking your class’s correctness to the internal evolution of another class. Composition is less romantic but more resilient: you build with replaceable parts, you keep boundaries narrow, and you can change behavior without rewriting your type hierarchy.\n\nWhen I’m optimizing for a codebase that can absorb product change—new pricing rules, new authorization policies, new integrations—composition is the default that keeps me out of trouble. When inheritance is genuinely modeling a stable “is-a” relationship, I’ll still use it, but I do it with eyes open and contracts written down.\n\n## Expansion Strategy\nAdd new sections or deepen existing ones with:\n\n- Deeper code examples: More complete, real-world implementations\n- Edge cases: What breaks and how to handle it\n- Practical scenarios: When to use vs when NOT to use\n- Performance considerations: Before/after comparisons (use ranges, not exact numbers)\n- Common pitfalls: Mistakes developers make and how to avoid them\n- Alternative approaches: Different ways to solve the same problem\n\n## If Relevant to Topic\n- Modern tooling and AI-assisted workflows (for infrastructure/framework topics)\n- Comparison tables for Traditional vs Modern approaches\n- Production considerations: deployment, monitoring, scaling\n


