architecture
Ports & Adapters, the toolkit way
The one rule
-core does not know that HTTP or a database exist. Everything else here is a consequence.
The payoff is not architectural purity, it is that business rules stay testable without a server or a container, and that replacing Exposed is one package of work rather than a rewrite. The cost is a mapping step at each boundary. Take that trade rather than relitigating it per endpoint.
The unusual thing about this layout is that the boundary is a Gradle module boundary, not a package convention. A package boundary is a promise; a module boundary is checked by the compiler on every build. Start there, because retrofitting it onto a service that grew inside one module is the expensive direction.
When the project is a single module
Plenty of services are one Gradle module, and most of this skill still applies — the placement rules work as package rules, with core, adapters
and app as packages instead of modules. Follow them that way. You lose the compiler check and keep everything else, which is most of the value.