1. One module, one reason to change
Split the codebase by capability—sync jobs, shortcodes, REST endpoints, admin settings—not by file length. Each module registers itself from a thin bootstrap file so you can disable or replace a slice without gutting the whole plugin. If two concerns always change together, they belong together; if not, draw a line.
2. WordPress hooks are your public API
Expose actions and filters at module boundaries instead of reaching into other classes’ internals. Document the stable ones in a short README or developer note. That is how site-specific code customizes behavior without forking your reusable package.
3. Keep presentation out of the domain layer
Business rules—validation, pricing, eligibility—live in plain PHP classes; markup and block editor glue live in adapters. When the next project uses a different theme or a headless front end, you swap the adapter, not the rules. Themes stay for layout; plugins own portable behavior.
4. Configuration over forks
Reusable features need sane defaults and explicit options: constants, filters, or settings screens—pick what matches the audience. Avoid “change the code for Client B”; that path does not scale. Version your settings schema when you must migrate options on upgrade.
5. Test and ship like a product
Even lightweight PHPUnit or integration checks around critical paths beat “it worked on my local.” Semantic versioning and a short changelog tell future you what is safe to reuse across hosts and PHP minor versions.
Modular plugin development is how reusable WordPress features stop being folklore and start behaving like internal products—bounded, extensible, and boring to deploy.