Rounds has a Kotlin server that integrates a few things:
- PostgreSQL persistence via SQLDelight (hosted on PlanetScale!)
- WebAuthn4J for Passkeys
- kotlinx.html for dynamic web pages
- Ktor for HTTP binding
The service uses six database tables. The business domain tables are Game and GameEvent. Support for auth, sessions, and collaborative editing adds Account, Cookie, Passkey, and GameAccess.
How many modules should a program have?
Decomposing code into modules is something I’ve struggled with for a long time.
One some projects I’ve screwed up by putting too much responsibility into a single module. Big modules are slow to build and test. Their size means they’ll change more frequently and will be rebuilt more frequently. Decomposing them into smaller modules is difficult because it requires introducing abstraction boundaries where there aren’t any. Consumers of big modules also suffer because they drag in unwanted behavior.
On other projects I’ve made the opposite mistake by having too many modules. Each module requires its own build file, where I paste repetitive configuration. I get particularly frustrated when a new feature depends on multiple too-small modules: do I introduce a new dependency between these modules? Or do I introduce yet another module that depends on the others? Consumers of my modules choose what they want à la carte, but must write verbose build files in the process.
Ralf’s Scheme
Ralf Wondratschek presented a pattern that was critical in decomposing the Square POS Android app. Each feature starts with three modules:
public: the feature’s public API interfaces and value objects.impl: the feature’s implementation classes. It provides service objects that implement the public API.impl-wiring: dependency injection glue that binds the interfaces inpublicto their implementations inimpl.
The full set of features are assembled together into an executable by an app module that depends on all of the impl-wiring modules.
Ralf also introduced a strict rule: projects can’t depend on each other’s impl and impl-wiring modules; they may only depend on each other’s public modules.
This scheme requires a lot of setup! But it yields compelling benefits:
- All
implmodules can build in parallel! - Changes to one
implmodule never causes otherimplmodules to rebuild. The build caches get lots of hits!
It’s simple and scalable and I wish I’d come up with it. Here’s what the scheme looks like for Rounds’ server. I’m using different words ’cause I dislike the word impl, and I put my wiring in the implementation module. I’m not using a dependency injection framework yet!
server
|-- accounts
| |-- api
| '-- real
|-- games
| |-- api
| '-- real
|-- passkeys
| |-- api
| '-- real
|-- server-development
|-- server-staging
'-- server-productionThe last 3 server-* modules build executables for each environment that Rounds runs in. Each depends on all of the real modules.
The games/api module depends on accounts/api because each GameAccess entity has an associated AccountId. Similarly, accounts/api depends on passkeys/api for PasskeyId.
Must Go Flatter
Most of the code is in real modules, and most of the code can build in parallel.
But I dislike dependencies between api modules. They build sequentially because there’s a dependency chain spanning them. I also have to carefully avoid creating a dependency cycle between these: it’s arbitrary that accounts/api depends on passkeys/api and not vice-versa!
When I look closer, the dependencies between my api modules is limited: I’ve got typesafe ID classes (AccountId, PasskeyId, etc.) and I reference these in my service interfaces. The fix is simple: introduce a new identifiers module with just that stuff:
server
|-- accounts
| |-- api
| '-- real
|-- games
| |-- api
| '-- real
|-- identifiers
|-- passkeys
| |-- api
| '-- real
|-- server-development
|-- server-staging
'-- server-productionNow the various *-api modules depend on identifiers, and it defines simple value objects for all of my features:
package app.rounds.identifiers
data class AccountId(val id: Long)
data class CookieId(val id: Long)
data class GameAccessId(val id: Long)
data class GameEventId(val id: Long)
data class GameId(val id: Long)
data class PasskeyId(val id: Long)The net result is a module dependency graph with nice symmetry and practical benefits.