Thursday, November 14, 2024

Modularize Spring Boot micro-service with Spring Modulith - Notes from my exploration . . .

The dictionary meaning of Modularity is - the use of individually distinct functional units, as in assembling an electronic or mechanical system. In other words, it is the degree to which components of a system can be separated. 

In Software Development, achieving modularity involves structuring various components involved into distinct modules or packages. The separation of components of various concerns is where Software Developers often fall into the traps of architectural layers. In other words, structure code that aligns with familiar technology layers like controller, service, persistence, domain etc by grouping similar kind of classes into a package with the name that mainly describes the logical layer. Structuring code by architectural layers that gives an architectural layered overview was once a good practice, but is quite common and a norm in modern application development. With this kind of code packaging structure, code within one package or layer gets interwoven with many functional/business use-cases. This also achieves modularity, but gets driven by the technical architecture, and not driven by business domain use-cases. The top-level domain or functional visibility is lost and pushed underneath architectural layers.

Spring Modulith

Spring Modulith is fairly new addition to Spring Projects. It not only helps bring in well-structured application modules that can be driven by domain, but also helps in verifying their arrangement, and even facilitates creating documentation.

It comes with few key fundamental arrangement/accessibility principles/rules/conventions with some limitations. The arrangement rules are considered violated only if you have a verification test in place. Otherwise, with your own arrangement, you will be able to generate documentation like PlantUML diagram showing Modules, their dependencies and boundaries.

Some points learned and noted

  • Modulith modules are analogous to Java packages.
  • Without making any modular/structural changes, if you add Modulith dependency and a unit test-case to verify, it will detect and report circular references which is very useful by itself.
  • By default each direct sub-package of the application main package is considered an application module, a.k.a module's base package.
  • Each module's base package is treated as an API package with all public classes under a module's base package made available for access and dependency injection from other modules.
  • Sub-packages under a module's base package are treated as internal to that and are not accessible by any other module, though Java allows if classes under that sub-package are public.
  • In order to expose or make a sub-package under a module's base package accessible to other packages, you need to provide a package-info.java file under that sub-package by annotating that package with @NamedInetrface in that file. With this the annotated sub-package which otherwise is treated as internal to the module that is under becomes available and is accessible from other application modules.
  • The file package-info.java is Java's standard way of adding package documentation which was introduced in Java 5. This file can contain Javadoc comment with Javadoc tags for the package along with package declaration and package annotations. Spring framework's null safety annotations like @NonNullFields, @NonNullApi can be specified in this file with which those specified annotations can be applied to all classes under that package.
  • By default the name of a module in UML generated is nothing but the module's base package name with the uppercase first letter. This name can be customized by annotating the package with @ApplicationModule and specifying the value for displayName property. This applies to only base package of the module. Sub-packages cannot be shown in the generated UML diagram anyway.
  • Additional customizations are possible. But I wouldn't overuse as it defeats simplicity.  
An example of package-info.java in a sub-package made accessible to other modules is described below:

Application main package: com.giri.myapp
Application module base package: event
Sub-package of module events exposed to other modules: publishers
The file package-info.java under com.giri.myapp.event.publisher looks like below: 
package-info.java
@org.springframework.modulith.NamedInterface package com.giri.myapp.event.publisher;

An example of package-info.java added to a module (base package) with different module name (EventConsumers) than the default name (Consumer) is shown below:
@org.springframework.modulith.ApplicationModule(displayName = "EventConsumers") package com.giri.myapp.consumer;

Limitations

  • Sub-package exposed as package cannot be shown in generated diagram.
Documentation can be generated by having a simple test-case as outlined in the documentation. UML diagram is particularly useful to get to see Architectural overview of the application modules, their dependencies and boundaries. However, if you organized any of the related components into sub-packages of a module base package and exposed those by annotating the package by adding package-info.java as shown above, it's quite natural to expect that the sub-package is shown in the generated UML as a module since it's treated as a module from exposure point of view for other modules. But the UML diagram seems only restricted and limited to showing the default application modules, the direct sub-packages under the project main package.

I did explore the API little bit to find out if there is a way to override this rule by any means, I couldn't find a way to do so. Hope the future version will consider this kind of expectation and provide an option for specifying for documentation as the sub-package exposed as a module by annotating the package with @NamedInterface is visible and used/depended on by other modules.
  • Generated visible diagram files should be treated as code to be checked in.
The clickable modules and visual diagram generated as .puml files are only useful to developers with IDE plugins. A mechanism to integrate into code documentation files like README.md or wiki would be more useful to let the Visible Architecture up-to-date with the codebase.
 

TIP

IntelliJ IDEA has a PlantUML Integration plugin available and can be used to view generated .puml files as UML diagram in the IDE. Follow these steps in order to generate Spring Modulith modules diagram and view the UML diagram in IntelliJ IDEA.
  • Make sure you have graphviz installed.
  • Install IntelliJ IDEA's PlantUML Integration plugin
  • Run test-case: ModularityTest which generates modulith PlantUML files (.puml) under application's target/spring-modulith-docs directory.
  • When you open any .puml file generated in IntelliJ, the plugin shows it as PlantUML diagram.
A sample JUnit test-case (ModularityTest.java) is shown below:
package com.giri.myapp.modularity; import com.giri.myapp.MyApplication; import org.junit.jupiter.api.Test; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.docs.Documenter; class ModularityTest { ApplicationModules modules = ApplicationModules.of(MyApplication.class); /** * Test to verify application structure. Rejects cyclic dependencies and access to internal types. */ @Test void verifyModularity() { System.out.println(modules); modules.verify(); } /** * Test to generate Application Module Component diagrams under target/spring-modulith-docs. */ @SuppressWarnings("squid:S2699") @Test void writeDocumentationSnippets() { new Documenter(modules) .writeModulesAsPlantUml() .writeIndividualModulesAsPlantUml(); } }

Summary

Structuring by domain use-cases vs architectural layers cannot be a personal preference. Viewing an application from domain aspect with underneath familiar technological layers gives a view that aligns better with business than viewing an application from technological layers. Spring Modulith helps achieve domain-driven modularity by following simple package level modular conventions where modules align with domain concepts.

Achieving better modularity doesn't necessarily need to be showing only business domain-based modules. Certain aspects of technologies like GraphQL let's say to indicate that the application provides GraphQL API for its client can as well get depicted in the generated UML module diagram by structuring controllers into a module with name graphql. With a right balanced mix of modularity, code can be restructured to show all main business/domain use-cases along with some technology related modules like Rest, GraphQL, Events etc. added to the mix. This kind of balanced approach gives good architectural and structural overview of the application showing how modules interact with each other, in some cases event showing high-level flow.

Spring Modulith comes with added support for structural validation, visual documentation of the modular arrangement, modular testability and observability.

Domain-driven modularity brings in more maintainable and understandable structure. However, keep things simple and do not overuse modularity, it defeats simplicity that Software Development is badly in need of. ;)

References




Friday, September 20, 2024

Java - Gotcha - Sealed interface and mocking in unit tests . . .

Seal(noun)
dictionary meaning - a device or substance that is used to join two things together so as to prevent them from coming apart or to prevent anything from passing between them. 

Prevent anything from passing between them. That's exactly what sometimes you want to put in place. When you have an interface and want to restrict other interfaces to extend or classes to implement to have a control on, you ned to seal your interface by specifying all those are permitted to extend or implement.

Java sealed interfaces is a feature introduced in Java 15 as a preview feature and became a standard feature in Java 17. Sealed interface restricts which classes or interfaces can implement or extend it. Classes that implement a sealed interface must be declared as final, sealed, or non-sealed. This provides more control over the inheritance hierarchy and helps to enforce certain design constraints.
 
To declare a sealed interface, use the sealed keyword followed by the permits clause, which lists the permitted subtypes.

E.g.
public sealed interface Shape permits Circle, Rectangle, Triangle { double area(); }

Each permitted subtype must be declared as one of the following:
  • Final: Cannot be extended further.
  • Sealed: Can specify its own permitted subtypes.
  • Non-Sealed: Removes the sealing restriction, allowing any class to extend it.
// Final class public final class Circle implements Shape { ... } // Sealed class public sealed class Rectangle implements Shape permits Square { ... } // Non-sealed class, additional permitted sub-type public final Square extends Rectangle { ... }

Benefits of Sealed Interfaces

Enhanced Control: Provide more control over the inheritance hierarchy, ensuring that only specific classes can implement the interface.
Improved Maintainability: By restricting the set of permitted subtypes, you can make your codebase easier to understand and maintain.
Better Exhaustiveness Checking: Sealed interfaces improve exhaustiveness checking in switch statements, especially when used with pattern matching (introduced in later Java versions).

The exhaustive checking in switch statement itself is very useful feature to have that makes your code not to miss handling a case of interface type in switch which otherwise is prone to bugs. The compiler would not let your code compile until all possible cases are handled in a switch statement making your code robust.

Shape aShape; ... switch(aShape) { case Circle circle -> circle.radius(); case Rectangle rectangle -> // do something // handle all remaining cases or provide default case, otherwise you code fails compilation }

Gotcha - Mockito, mocking sealed interface

Mocking is common in unit testing. If you are writing unit test for an object A that depends on object B, you will not be interested in B and can simply mock it's behavior. If Mockito is your mocking framework, and B happens to be a sealed interface with some permitted implementations, then you will not be able to mock like usually you do as follows:

class ATest { ... @Mock private B objB; ... }

Your test fails with the following error when it is run:
org.mockito.exceptions.base.MockitoException: Mockito cannot mock this class: interface B. If you're not sure why you're getting this error, please open an issue on GitHub. Java : 22 JVM vendor name : Amazon.com Inc. JVM vendor version : 22.0.1+8-FR JVM name : OpenJDK 64-Bit Server VM JVM version : 22.0.1+8-FR JVM info : mixed mode, sharing OS name : Mac OS X OS version : 13.6.6 You are seeing this disclaimer because Mockito is configured to create inlined mocks. You can learn about inline mocks and their limitations under item #39 of the Mockito class javadoc. Underlying exception : org.mockito.exceptions.base.MockitoException: Unsupported settings with this type 'B'

Solution
Change mock to a specific implementation of the interface.
private final B objB = mock(BImpl.class); // sealed interface, specify specific implementation class to be mocked

Monday, September 16, 2024

Spring Code TIP-2: Assert expected log message in a test-case . . .

Logging is helpful to know the insights of a running application. It is also useful in investigating issues. Sometimes, we would like to log certain configuration details after the application starts up. This can be achieved by implementing functional interface CommandLineRunner. Any bean that implements this interface is a special type of bean that runs after the application context is fully loaded.

CommandLineRunner is a functional interface with a single method run(String... args). Beans implementing this interface are executed after the application context is loaded and before the Spring Boot application starts.

For example the following application class defines a CommandLineRunner bean. The bean returns a lambda expression which defines the behavior of the CommandLineRunner. As an altervative to defining a bean, the MyApplication class can also implement this interface and provide implementation for run method. As shown below, this bean checks and if the jdbcClient is available, it executes a SQL query to get the database version and logs the result. This is run after the application context is fully loaded so that we know the version of the Database that the application is using. This is one such very useful information.

@SpringBootApplication @Slf4j public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } @Bean public CommandLineRunner initializationChecks(@Autowired(required = false) JdbcClient jdbcClient) { return args -> { if (jdbcClient != null) { log.info("Database check: {}", jdbcClient.sql("SELECT version()").query(String.class).single()); } }; } }

Now, say we want to assert this log message in a test-case. That way we ensure that the log message contains the expected version of the Database, and the Database won't get changed/upgraded without a test-case catching it.

Spring Code TIP - test log message

JUnit's @ExtendsWith annotation and Spring Boot's OutputCaptureExtension can be leveraged to  achieve this.

The following is an integration test, that does this:
@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS) @ExtendWith(OutputCaptureExtension.class) class MyApplicationIntegrationTest { @Autowired ApplicationContext applicationContext; @Autowired MyService myService; @Test @DisplayName("An integration Smoke Test to ensure that the application context loads, autowiring works, and checks DB version.") void smokeTest_context_loads_and_autowiring_works(CapturedOutput output) { var otherService = applicationContext.getBean(OtherService.class); assertThat(otherService).isInstanceOf(OtherService.class); assertThat(myService).isNotNull(); assertThat(output).contains("Database version: PostgreSQL 15.3"); } }

Highlighted is the code in focus, assuming that the Database used is PostgreSQL 15.3.