A team building several related products will repeatedly encounter similar code. Moving every similarity into the core can make the platform hard to change, while leaving every repeated rule in a leaf part creates drift. The decision is about ownership and stability, not simply counting copies. A shared rule belongs in the core when its meaning is genuinely common, its contract is clear, and centralizing it reduces risk or effort across products.
Identify the repeated decision
Look beyond identical syntax. Two parts may both filter records by tenant, and that is a common platform rule. Two parts may both calculate a price, but their business meanings may differ. Ask whether a future correction should change every use in the same way. If yes, a shared implementation is plausible. If each product needs a different policy, a common-looking helper can conceal divergence and force conditionals into the core. Write down the invariant before extracting code so the team knows what the core promises.
Check who owns the rule
Core layers are appropriate for cross-cutting concerns such as item ID generation, cache-key hierarchy, localization lookup, response handling, and tenant context. A blog publication policy belongs to the blog part even if two runnable products use that part. A restaurant-specific table rule belongs in the restaurant product. If the core must import a leaf to implement a rule, the dependency direction is probably wrong. Move only the general mechanism into the core and let the part provide the domain data or policy through a public contract.
Wait for evidence, but not too long
One use case may not reveal a stable abstraction. A second independent use often exposes which behavior is common and which is incidental. Waiting until many copies exist can make migration costly, particularly for security-sensitive rules. The team can use a small interim helper within one part, then promote it when another part needs the same invariant. Record the expected consumers and the cost of leaving the rule duplicated. A shared authorization or tenant isolation rule may deserve earlier centralization than a minor formatting detail because inconsistent copies create greater risk.
Design a narrow interface
The core should expose the smallest operation that expresses the shared rule. Prefer inputs and outputs with clear meaning over a large options object that attempts to cover every future variation. Keep domain-specific configuration outside the implementation and provide a deliberate extension point when real products need one. A useful test is whether a new part can adopt the abstraction without learning another part's vocabulary. If the interface contains many flags named after current customers, it likely moved too much specific behavior into the core.
Plan the migration
When promoting a rule, update existing callers to use the new implementation and remove the old copies. Keep observable behavior stable, especially IDs, URLs, cached values, and API response shapes. Run focused tests for the shared invariant and integration tests in composed products. Check that bundling still keeps core and part boundaries separate. A partial migration can be worse than duplication because some consumers follow the new rule while others silently retain the old one. Give the change an owner who can find all uses and verify adoption.
Revisit the abstraction
A core rule is a long-lived promise. Watch how new consumers use it and whether they need repeated exceptions. If the abstraction grows conditional branches for unrelated domains, split the shared mechanism from the policies or move specific behavior back to its owner. The root-to-leaf complexity gradient works when common complexity lives centrally and leaves remain simple, not when the core becomes a warehouse of every feature. The right moment to centralize is when the common meaning is proven and the resulting contract makes real products easier to maintain. Ask whether the next product could use the rule unchanged; that is a stronger signal than superficial similarity in two files.