ChestShop is upstream's "Bukkit events as internal middleware" design: a single buy/sell is orchestrated across ~15 listeners coordinated only by @EventHandler(priority) ordering and a giant mutable PreTransactionEvent/TransactionEvent bag — no atomic boundary (ADT-4), untestable, the lone outlier vs. the monorepo's entrypoint→service→DAO. Persistence is SQLite/ORMlite via static God-classes (NameManager, ItemDatabase).
Strangler-fig migration (each phase ships independently, plugin works throughout). Storage stays SQLite for now behind storage-agnostic DAO interfaces (swap to shared MariaDB later = a new impl).
ChestShopModule (Guice), ItemCodeRepository (DAO interface) + SqliteItemCodeRepository (impl over the ORMlite items.db), and an ItemCodeService that owns the serialise/find-or-create/metadata-migration logic. Deleted the ItemDatabase God-class; callers go through ChestShop.itemCodes(). Faithful refactor (item codes byte-identical). Enables cleanly.TransactionService.execute(...) owning money+items+record atomically, collapsing the post-transaction listener chain.ShopService for creation/removal. Phase 3: delete the dead event scaffolding. Account domain (NameManager/users.db) gets the same DAO/service treatment.Phase 2-shops done (commit 2f8e28f): ShopService extracted. Shops have no ChestShop-owned persistence (sign+chest world state; only DB is Treasury's external MarketApi), so no repository — just a service owning the lifecycle money logic that was embedded in listeners: chargeCreationFee(player, signLines) (fee + server-economy credit + message, returns whether creation proceeds) and refundOnRemoval(destroyer, sign) (refund + server-economy debit + message). CreationFeeGetter and ShopRefundListener are now thin entrypoints delegating via ChestShop.shops(). Money still flows through the Currency*Event integration points. The 12-listener creation-validation pipeline + market sync stay as events (deliberately — integration/coordination, Phase 3). Builds + tests green.
Remaining: migrate the ~25 callers off the NameManager static facade and delete it (split its 2 @EventHandlers into a thin AccountListener); then Phase 3 — assess collapsing the internal-only validation scaffolding (PreShopCreationEvent pipeline) now that the money logic is out.
NameManager facade deleted (commit 5d68858): account-domain migration complete. The ~25 callers across the economy/sign-creation/removal/command paths now call AccountService directly via ChestShop.accounts(); the two account @EventHandlers moved to a thin AccountListener (registered in place of the old NameManager listener). Pure delegator removal, no behaviour change. Builds + tests green.
Remaining: Phase 3 — assess whether the now internal-only shop-creation validation scaffolding (PreShopCreationEvent + the 12 preshopcreation listeners) is worth collapsing into the ShopService, now that the money logic is out and the genuine integration events (AccountQuery/AccountAccess, Currency*, ShopCreated/Destroyed→Market) are clearly separated.
Phase 2 done (commit 7d0dc3f): account domain migrated.
NameManagerstatic God-class split intoAccountRepository+SqliteAccountRepository(users.db persistence behind a storage-agnostic contract) andAccountService(@Singleton: caches, get-or-create, shortened-name allocation, admin/server-economy accounts, canUseName, load()), reached viaChestShop.accounts().NameManageris now a thin Listener (the AccountQuery/AccountAccess events stay — Treasury integration points) plus a delegating static facade so the ~25 callers compile unchanged. AddedAccountServiceTest(row-count + shortened-name disambiguation now unit-testable over a fake repo). Builds + tests green.Remaining: ShopService for creation/removal (Phase 2-shops), migrate the ~25 callers off the NameManager facade and delete it, then delete the dead event scaffolding (Phase 3).