Improving a Legacy Insurance Model Without Breaking a Generated API
An insurer hands you a 240-class UML model of policy administration, generated into 180,000 lines of Java. 61% of association ends have unspecified navigability, nine packages form dependency cycles, and Policy has 78 attributes and 41 operations. The previous vendor left almost no behavioral models; only a few sequence diagrams cover quote creation. Code generation emits a field for every navigable end and a getter/setter for every attribute, so the build compiles but the persistence layer serializes about 30% more columns than needed. Management wants a six-week cleanup with no functional change, and the runtime team refuses any public API signature change in the next release. Two architects disagree: one wants to extract a PolicyGeneralization hierarchy immediately, the other wants to split Policy by composition.
1. Propose a prioritized six-week plan. What do you fix first, and what evidence justifies the order?
Read the worked answer
Start with measurement, not restructuring. Establish a baseline: count associations with unspecified navigability, compute package dependency cycles, list unused generated columns, and record the public API signatures that must not change. The first fix should be navigability because it is high-impact and low-risk: making ends explicit changes generated fields and serialization, not method signatures, and it directly reduces the 30% column overhead. Use the persistence traces and query logs to decide which ends are actually traversed rather than guessing. Second, break package cycles at the dependency level, typically by moving shared abstractions into a foundation package or introducing an interface owned by the dependent package. Third, address Policy, but only after the API freeze list is agreed. Extract a behavioral contract or facade that preserves existing signatures, then refactor internals behind it. Defer the generalization-versus-composition decision until you have evidence of substitutability and lifecycle. Track weekly metrics so the six-week claim is testable, and gate each step with generated-code diffing and regression tests rather than model aesthetics.
2. How should the team decide between extracting a generalization hierarchy and decomposing Policy by composition? Give criteria, not a preference.
Read the worked answer
Generalization is justified only when every subtype is substitutable for Policy in all contexts, inherits the same invariants, and does not need to reject inherited operations. If the 78 attributes cluster into distinct policy lines with different lifecycles and rules, and many attributes are meaningless for some lines, generalization will produce sparse subtypes and conditional behavior. Composition is preferable when the clusters are roles or parts that can vary independently, such as coverage, billing account, and risk assessment. Use evidence: attribute usage per policy line, null rates in persisted data, operations that branch on policy type, and the number of client call sites that would need to change. Check whether a proposed subtype would inherit operations it cannot support; that is a strong signal against generalization. Also consider identity: a policy remains the same policy when its coverage changes, which favors composition of coverage objects over subclassing. Choose the structure that preserves the existing public API through delegation, and document the decision with the criteria and counterexamples.
3. The runtime team refuses API changes. How can package cycles and the Policy god class be improved while preserving signatures?
Read the worked answer
Preserve signatures at the boundary and move change inward. For Policy, introduce a facade or adapter that retains every existing public method and delegates to new internal collaborators; the old methods remain as deprecated wrappers, and new structure lives behind them. For cycles, identify the smallest set of types that both packages need and move them to a lower-level package, or define an interface in the client package and have the provider implement it, inverting the compile-time dependency without changing runtime call signatures. Generated getters and setters can remain while fields move into composed objects, as long as the accessors delegate. This is not free: serialization formats and reflection-based frameworks may notice the change, so add contract tests that compare serialized output and API signatures before and after. Use a dependency-structure metric and a generated-code diff as evidence. If a cycle cannot be broken without changing a public return type, leave it and document the exception rather than forcing a risky change in a six-week window.