Bounding clean architecture components with Spring Boot and ArchUnit

When we develop software, we want to create "- spine ": I see spine , maintainability spine , extend spine , and - in a trend now - decomposition (the ability to expand the monolith on mikroservisy, if necessary). Add to the list of your favorite 'ability spine . "

Most - perhaps even all - of these "features" go hand in hand with pure dependencies between components.

If a component depends on all other components, we do not know what side effects changing one component will have, which makes it difficult to maintain the codebase and makes it even more difficult to extend and decompose.

Over time, the boundaries of components in the codebase tend to blur. Bad dependencies appear, making it harder to work with the code. This has all kinds of bad consequences. In particular, development is slowing down.

This is all the more important if we are working on a monolithic codebase that spans many different business areas or "bounded contexts" to use Domain-Driven Design jargon.

How can we protect our codebase from unwanted dependencies? With careful design of bounded contexts and constant adherence to component boundaries. This article demonstrates a set of practices that help you in both cases when working with Spring Boot.

 Sample code

This article is accompanied by a sample working code  on GitHub  .

Package-Private Visibility

What helps with keeping component boundaries? Reduced visibility.

If we use Package-Private visibility for "inner" classes, only the classes in the same package will have access. This makes it difficult to add unwanted dependencies from outside the package.

, , .  ?

, .

, .

, , .

! , , . , , , .  !

, , package-private , , , .

?  package-private .  , package-private , , ArchUnit , package-private .

. , , :

.  .

Domain-Driven Design (DDD): , .  , .  ยซยป ยซ ยป .

, .  .

: , .  .  public , , .

API

, :

billing
โ”œโ”€โ”€ api
โ””โ”€โ”€ internal
    โ”œโ”€โ”€ batchjob
    |   โ””โ”€โ”€ internal
    โ””โ”€โ”€ database
        โ”œโ”€โ”€ api
        โ””โ”€โ”€ internal

 internal, , , ,  api, , , API, .

 internal api :

  • .

  • ,  internal .

  • ,  internal .

  • api internal ArchUnit (  ).

  •   api  internal, , - .

,  internal package-private.  public ( public, ), .

, Java package-private , , .

.

Package-Private

 database:

database
โ”œโ”€โ”€ api
|   โ”œโ”€โ”€ + LineItem
|   โ”œโ”€โ”€ + ReadLineItems
|   โ””โ”€โ”€ + WriteLineItems
โ””โ”€โ”€ internal
    โ””โ”€โ”€ o BillingDatabase

+, public, o, package-private.

database API  ReadLineItems WriteLineItems, , .  LineItem API.

 databaseBillingDatabase :

@Component
class BillingDatabase implements WriteLineItems, ReadLineItems {
  ...
}

, .

, .

 api,  internal, .   internal , ,  api.

 database, , , .

 batchjob:

batchjob API .   LoadInvoiceDataBatchJob(, , ), ,  WriteLineItems:

@Component
@RequiredArgsConstructor
class LoadInvoiceDataBatchJob {

  private final WriteLineItems writeLineItems;

  @Scheduled(fixedRate = 5000)
  void loadDataFromBillingSystem() {
    ...
    writeLineItems.saveLineItems(items);
  }
}

,  @Scheduled Spring,  .

,  billing:

billing
โ”œโ”€โ”€ api
|   โ”œโ”€โ”€ + Invoice
|   โ””โ”€โ”€ + InvoiceCalculator
โ””โ”€โ”€ internal
    โ”œโ”€โ”€ batchjob
    โ”œโ”€โ”€ database
    โ””โ”€โ”€ o BillingService

billing InvoiceCalculator  Invoice.  ,  InvoiceCalculator ,  BillingServiceBillingService  ReadLineItemsAPI - :

@Component
@RequiredArgsConstructor
class BillingService implements InvoiceCalculator {

  private final ReadLineItems readLineItems;

  @Override
  public Invoice calculateInvoice(
        Long userId, 
        LocalDate fromDate, 
        LocalDate toDate) {
    
    List<LineItem> items = readLineItems.getLineItemsForUser(
      userId, 
      fromDate, 
      toDate);
    ... 
  }
}

, , , .

Spring Boot

, Spring Java Config  Configuration  internal   :

billing
โ””โ”€โ”€ internal
    โ”œโ”€โ”€ batchjob
    |   โ””โ”€โ”€ internal
    |       โ””โ”€โ”€ o BillingBatchJobConfiguration
    โ”œโ”€โ”€ database
    |   โ””โ”€โ”€ internal
    |       โ””โ”€โ”€ o BillingDatabaseConfiguration
    โ””โ”€โ”€ o BillingConfiguration

Spring Spring .

database :

@Configuration
@EnableJpaRepositories
@ComponentScan
class BillingDatabaseConfiguration {

}

@Configuration Spring, , Spring .

@ComponentScan Spring, ,  ,   ( )  @Component .   BillingDatabase, .

@ComponentScan  @Bean  @Configuration.

 database Spring Data JPA.  @EnableJpaRepositories.

batchjob  :

@Configuration
@EnableScheduling
@ComponentScan
class BillingBatchJobConfiguration {

}

@EnableScheduling.  , @Scheduled bean-LoadInvoiceDataBatchJob.

,  billing :

@Configuration
@ComponentScan
class BillingConfiguration {

}

@ComponentScan ,  @Configuration Spring bean-.

, Spring .

, ,  @Configuration. , :

  • ()   SpringBootTest.

  • () ,   @Conditional... .

  • , , () , () .

:  billing.internal.database.api public,  billing, .

, ArchUnit.

ArchUnit

ArchUnit - , .  , , .

,  internal .  ,  billing.internal.*.api  billing.internal.

 internal , - ยซยป.

( ยซinternalยป ), ,  @InternalPackage:

@Target(ElementType.PACKAGE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InternalPackage {

}

 package-info.java :

@InternalPackage
package io.reflectoring.boundaries.billing.internal.database.internal;

import io.reflectoring.boundaries.InternalPackage;

, , .

, , :

class InternalPackageTests {

  private static final String BASE_PACKAGE = "io.reflectoring";
  private final JavaClasses analyzedClasses = 
      new ClassFileImporter().importPackages(BASE_PACKAGE);

  @Test
  void internalPackagesAreNotAccessedFromOutside() throws IOException {

    List<String> internalPackages = internalPackages(BASE_PACKAGE);

    for (String internalPackage : internalPackages) {
      assertPackageIsNotAccessedFromOutside(internalPackage);
    }
  }

  private List<String> internalPackages(String basePackage) {
    Reflections reflections = new Reflections(basePackage);
    return reflections.getTypesAnnotatedWith(InternalPackage.class).stream()
        .map(c -> c.getPackage().getName())
        .collect(Collectors.toList());
  }

  void assertPackageIsNotAccessedFromOutside(String internalPackage) {
    noClasses()
        .that()
        .resideOutsideOfPackage(packageMatcher(internalPackage))
        .should()
        .dependOnClassesThat()
        .resideInAPackage(packageMatcher(internalPackage))
        .check(analyzedClasses);
  }

  private String packageMatcher(String fullyQualifiedPackage) {
    return fullyQualifiedPackage + "..";
  }
}

 internalPackages(), reflection ,  @InternalPackage.

 assertPackageIsNotAccessedFromOutside().  API- ArchUnit, DSL, , ยซ, , , ยป.

, - public  .

: , (io.reflectoring ) ?

, ( ) io.reflectoring.  , .

, .

, :

class InternalPackageTests {

  private static final String BASE_PACKAGE = "io.reflectoring";

  @Test
  void internalPackagesAreNotAccessedFromOutside() throws IOException {

    // make it refactoring-safe in case we're renaming the base package
    assertPackageExists(BASE_PACKAGE);

    List<String> internalPackages = internalPackages(BASE_PACKAGE);

    for (String internalPackage : internalPackages) {
      // make it refactoring-safe in case we're renaming the internal package
      assertPackageIsNotAccessedFromOutside(internalPackage);
    }
  }

  void assertPackageExists(String packageName) {
    assertThat(analyzedClasses.containPackage(packageName))
        .as("package %s exists", packageName)
        .isTrue();
  }

  private List<String> internalPackages(String basePackage) {
    ...
  }

  void assertPackageIsNotAccessedFromOutside(String internalPackage) {
    ...
  }
}

 assertPackageExists() ArchUnit, , , .

.  , , .  ,  @InternalPackage  internalPackages().

, .

Java- Spring Boot ArchUnit , - .

API , .

!

, ,  GitHub .

Spring Boot,   moduliths.




All Articles