xmolecules / jmolecules

Libraries to help developers express architectural abstractions in Java code
http://jmolecules.org
Apache License 2.0
1.23k stars 103 forks source link

Allow architecture annotations to be used on Java modules #128

Open xenoterracide opened 4 weeks ago

xenoterracide commented 4 weeks ago

adding, e.g. @DomainLayer, to a package is cool, but it would be nice to add that to a module-info.java. That would look something like this from a consumer perspective, and should affect all packages inside of the module. I think individual packages should be able to be considered to override this. For example my security package might have an unexported (or not exported to everything) nested package that is for infrastructure, but that's less of concern to people consuming the module because they can't see it anyways.

import org.jmolecules.architecture.layered.DomainLayer;
import org.jspecify.annotations.NullMarked;

/**
 * Security model.
 */
@DomainLayer
@NullMarked module com.xenoterracide.model.security {
  requires static org.jmolecules.architecture.layered;
  requires static org.jspecify;
}

I could look into this, but no promises. It's not hard to let the annotation be on the module, the problem is making tooling recognize it.

odrotbohm commented 3 weeks ago

Wouldn't it make more sense to annotate the packages exposed by a module, then?

xenoterracide commented 3 weeks ago

I'm not entirely sure how some of these annotations are meant to be applied.

In this case, my intention is that all packages should be considered part of the @DomainLayer by default, and only when a package is explicitly annotated otherwise should it be treated differently. This mirrors how jspecify (for better or worse) assumes everything is non-null unless marked as @Nullable. I don't currently see a scenario where I’d need to annotate a module one way and override it elsewhere, but I appreciate having the flexibility if necessary.

A major source of confusion is the @Module annotation. There are now four different concepts of "module" in my system, and it's unclear whether they can exist within the same bounded context. For example, Maven treats a JAR build as a "module," meaning a multi-module setup involves multiple JARs (what Gradle refers to as "projects"). JPMS defines a module as a descriptor for the JAR. Then there’s this annotation, plus the notion of a module in Modulith’s application framework. Since JPMS didn’t exist when DDD was coined, it’s uncertain if the term "module" would have been used in this way. Having reviewed the "modules" section of the blue book, it seems reasonable to have a singular domain model layer JAR for something like "customer," while placing non-domain concerns, such as the User Interface Layer, elsewhere. The chapter only discourages splitting the model itself, not separating concerns across layers.

The documentation around when and why to use each type of module feels lacking. While I recognize that these are DDD concepts that didn’t originally have annotations, I don’t believe a bounded context was intended to be applied to a package. It’s also unclear if @BoundedContext should be applied to a package alongside @Module and @DomainLayer, or if each annotation is meant to be unique to the package.

I also doubt that a bounded context (@BoundedContext) can always map directly to a single JAR. I’m finding myself unsure about how to properly apply @BoundedContext and @Module, especially since I’ve split my JARs into layers and slices. For example, within my security bounded context, I have two layer JARs: com.xenoterracide.security.{model,controller}. Should I be creating a separate JAR or package just for com.xenoterracide.security to apply the @BoundedContext annotation?

Interestingly, this could theoretically introduce additional compile-time constraints. For instance, if AggregateRoots were placed in the root exported package and all non-API supporting entities were in another package, this would prevent direct access to those entities from other layers. This setup would enforce that all operations must go through the aggregate root or an exposed domain service.

xenoterracide commented 2 weeks ago

I've been reflecting on how I would adhere to such designs if JPMS modules were permitted. Here's what I'm thinking:

  1. Conceptually, @Module and a JPMS module are equivalent.

    • In alignment with the Blue Book, the domain model should remain intact and separate from concerns like user interface and infrastructure. Therefore, a module should clearly define its layer.
    • As a validation step, no package exported by the module should belong to a different layer. There might be questions around whether a DomainLayer module can contain an InfrastructureLayer package or class. For instance, I have a package-protected class for generating builders. Even if it's in an exported package, it's not accessible outside the package itself. I think this is valid, and arguably, it could even be considered part of the domain layer since it's essentially a "factory."
  2. I believe there should be exactly one @BoundedContext at the @DomainLayer, which should also exist at the module level.

Thus, any @DomainLayer module should be annotated accordingly. To simplify things, it might make sense to introduce an @DomainBoundedContext annotation that applies to the module and combines all three annotations (similar to how Spring allows composable annotations).

@BoundedContext
@DomainLayer
@Module
module tld.myorg.security {...}

Examples:

/**
 * A rough explanation, possibly covering why security is its own domain.
 **/
@BoundedContext(name = "Security")
@DomainLayer
@Module
module tld.myorg.security {
  exports tld.myorg.security;
  exports tld.myorg.security.user;
  opens to whatever;
}
@Aggregate // Missing concept, possibly for another ticket/issue
// Automatically part of the Domain Layer within the security context/module
package tld.myorg.security.user;
@UserInterfaceLayer // Because this could use a rename ;)
@Module
module tld.myorg.security.controller {
   // No EXPORT!!! ;) because nothing should import these in a Spring app
   opens tld.myorg.security.controller to spring...;
}

At this point, I'm uncertain whether the user interface should be scoped to a single bounded context. My hesitation likely stems from the belief that a well-structured monolithic application with proper modules can be split into multiple microservices by creating projects that depend on different "user interface" layers. These modules would then be composed into an application project.

I understand not everyone may agree with placing controllers in a separate module or package from the feature slice, but it does help keep business logic out of the controllers. It also makes swapping implementations—such as replacing REST with GraphQL or supporting both—more manageable.

P.S I've been reflecting on how I would adhere to such designs if JPMS modules were permitted. Here's what I'm thinking:

  1. Conceptually, @Module and a JPMS module are equivalent.

    • In alignment with the Blue Book, the domain model should remain intact and separate from concerns like user interface and infrastructure. Therefore, a module should clearly define its layer.
    • As a validation step, no package exported by the module should belong to a different layer. There might be questions around whether a DomainLayer module can contain an InfrastructureLayer package or class. For instance, I have a package-protected class for generating builders. Even if it's in an exported package, it's not accessible outside the package itself. I think this is valid, and arguably, it could even be considered part of the domain layer since it's essentially a "factory."
  2. I believe there should be exactly one @BoundedContext at the @DomainLayer, which should also exist at the module level.

Thus, any @DomainLayer module should be annotated accordingly. To simplify things, it might make sense to introduce an @DomainBoundedContext annotation that applies to the module and combines all three annotations (similar to how Spring allows composable annotations).

@BoundedContext
@DomainLayer
@Module
module tld.myorg.security {...}

Examples:

/**
 * A rough explanation, possibly covering why security is its own domain.
 **/
@BoundedContext(name = "Security")
@DomainLayer
@Module
module tld.myorg.security {
  exports tld.myorg.security;
  exports tld.myorg.security.user;
  opens to whatever;
}
@Aggregate // Missing concept, possibly for another ticket/issue
// Automatically part of the Domain Layer within the security context/module
package tld.myorg.security.user;
@UserInterfaceLayer // Because this could use a rename ;)
@Module
module tld.myorg.security.controller {
   // No EXPORT!!! ;) because nothing should import these in a Spring app
   opens tld.myorg.security.controller to spring...;
}

At this point, I'm uncertain whether the user interface should be scoped to a single bounded context. My hesitation likely stems from the belief that a well-structured monolithic application with proper modules can be split into multiple microservices by creating projects that depend on different "user interface" layers. These modules would then be composed into an application project.

I understand not everyone may agree with placing controllers in a separate module or package from the feature slice, but it does help keep business logic out of the controllers. It also makes swapping implementations—such as replacing REST with GraphQL or supporting both—more manageable.

P.S. for completeness this is class that I'm not certain if it's infrastructure or domain layer. I lean towards Infrastructure due to the "not for direct use", at the same time more complicated versions of this class could contain business logic for contruction.

package com.xenoterracide.security.user;

import jakarta.annotation.Nonnull;
import java.util.HashSet;
import java.util.Set;
import org.immutables.builder.Builder;
import org.immutables.value.Value;
import org.jmolecules.architecture.layered.InfrastructureLayer;
import org.jspecify.annotations.NonNull;

/**
 * Do not use directly, this class is for generating builders that you should use instead.
 */
@InfrastructureLayer
@Value.Style(newBuilder = "create", jakarta = true, jdk9Collections = true, jdkOnly = true)
final class UserFactory {

  private UserFactory() {}

  @Builder.Factory
  static User user(@NonNull String name, @NonNull Set<IdentityProviderUser> identityProviderUsers) {
    return new User(User.UserId.create(), name, new HashSet<>(identityProviderUsers));
  }
}
odrotbohm commented 2 weeks ago

I appreciate your input, but be reminded that this is a bug tracker, not a discussion forum how to use the annotations. So I deliberately skip the brain dump on modules layers etc. Furthermore, please recognize that JPMS is only rarely used in real-world applications, so it cannot be the center of our design attention.

I still think that the currents state of affairs is just fine, because the annotations that make sense on packages can already be used there. They are also already detected in exactly that place by relevant integration technology. What you're suggesting here is being able to use those annotations on module-info.java compared to package-info.java.

The essential question to be answered is what this step would enable that's currently impossible, except "being able to declare the annotation in a different file". If we find an answer to that question, I'm happy to further investigate. In the absence of a proper answer, I'd rather stay with the current state of affairs.

xenoterracide commented 2 weeks ago

but it is a place to suggest improving documentation. I'm brain dumping because I don't know how to develop a comprehensive list of annotations that belong on modules; as from much of the documentation it's not clear what the intent is.

The essential question to be answered is what this step would enable that's currently impossible, except "being able to declare the annotation in a different file". If we find an answer to that question, I'm happy to further investigate. In the absence of a proper answer, I'd rather stay with the current state of affairs.

I'm not certain I understand. The only step needed, assuming the annotations are for documentation purposes is to allow them to annotate a module. Then a given annotation should be applicable on a module

@Target({
  ElementType.MODULE
})

after that, consumers would also need to look at this.getClass().getModule().getAnnotation()(s), but that's on the consumers, which I don't think? this particular repo provides.