Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow architecture annotations to be used on Java modules #128

Open
xenoterracide opened this issue Sep 14, 2024 · 5 comments
Open

Allow architecture annotations to be used on Java modules #128

xenoterracide opened this issue Sep 14, 2024 · 5 comments
Labels
lifecycle: waiting-for-feedback Feedback by the original reporter needed module: architecture Architectural style related support type: enhancement New feature or request

Comments

@xenoterracide
Copy link

xenoterracide commented Sep 14, 2024

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 odrotbohm added type: enhancement New feature or request module: architecture Architectural style related support lifecycle: waiting-for-feedback Feedback by the original reporter needed labels Sep 20, 2024
@odrotbohm
Copy link
Member

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

@odrotbohm odrotbohm changed the title JPMS: support annotating modules Allow architecture annotations to be used on Java modules Sep 20, 2024
@xenoterracide
Copy link
Author

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
Copy link
Author

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
Copy link
Member

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
Copy link
Author

xenoterracide commented Sep 22, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lifecycle: waiting-for-feedback Feedback by the original reporter needed module: architecture Architectural style related support type: enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants