PUBLIC OBJECT

Flattening my Dependency Graph

Rounds has a Kotlin server that integrates a few things:

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 in public to their implementations in impl.

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 impl modules can build in parallel!
  • Changes to one impl module never causes other impl modules 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-production

The 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-production

Now 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.