1

Assume we have a single large JVM project (the example is in Kotlin), containing code. As part of a refactoring effort, we are decoupling pieces of the code by splitting the code into multiple modules, and using a build tool such as Gradle/Maven to build deploy applications with only a small set of the modules.

Some more details:

class SomeService(
  private val SomeCallable: callable, // implemented by OtherService during runtime
) {
  fun doSomething() {
    callable.call()
  }
}

interface SomeCallable {
  fun call()
}

class OtherService: SomeCallable {
  fun call() {}
}

We want to split this code into two modules:

// Module A
class SomeService(
  private val OtherService: callable,
) {
  fun doSomething() {
    callable.call()
  }
}

// Module B
class OtherService: SomeCallable {
  fun call() {}
}

// Module ???
interface SomeCallable {
  fun call()
}

The question is in which module the interface belongs. If it would be part of module A, then the implementation in module B cannot compile. If it would be part of module B, then the calling service cannot compile. It seems we need a third module C containing just the interface SomeCallable, and that both modules A and B depend on.

A compilation dependency diagram:

A   B
 \ /
  C 

A runtime dependency diagram:

A
| \  
B  |
| / 
C

Is this the best way to achieve the decoupling of code/modules, or are there better ways?

1 Answer 1

1

Your reasoning seems correct, though a simpler practical solution might be to combine B and C, depending on the details in your case.


In that design, A would depend on (B+C) at both compile time and runtime. This obviously couples A to the implementation of the service, which isn't conceptually ideal, but may be worthwhile in practical terms in order to simplify your module hierarchy.

A would require B in order to run, but from your runtime depencency diagram, that was the case anyway, so that hasn't changed. However, it does mean that it's more complicated to swap out B for another implementation of C, so if you were planning on doing that, C as a separate module makes more sense.

It does open the possibility that developers may misunderstand or work around the design, and introduce an unwanted compile time dependency from A onto B directly, in the absence of a hard module boundary preventing them. However, there are ways to prevent that, for example Java 9 modularity (JPMS). The (B+C) module could declare only the C packages as exports, therefore hiding the B packages from anything outside of that module.

It's also worth noting that A would not be able to instantiate OtherService from B directly, using your original design. That would need handling in some way, either by a dependency injection framework, an additional module D that depends on both A and B, ServiceLoader with JPMS uses and provides, or something similar.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.