admin管理员组

文章数量:1410737

I am trying to understand how to externalize Spring Modulith application events via Kafka, but I can't get it to work properly. The problem is that the application events are not being sent via Kafka. Nothing is happening at the moment, and I am not getting any exceptions either, which makes it hard to debug since there are no clear hints on what is going wrong.

I have provided the complete Spring Boot application on GitHub:

In src/main/resources/environment, you will find two Docker Compose scripts — one for Kafka and one for MariaDB. The application starts correctly, and via REST API, you can create an account and perform financial transactions. However, no events are being published to Kafka.

Has anyone encountered a similar issue, or does anyone have any hints or advice on what I might be missing? Any help would be greatly appreciated!


I added this ApplicationModuleTest to make clear what should happen in the workflow.

package de.homebrewed.financemanager.workflow;

import static .assertj.core.api.Assertions.assertThat;

import de.homebrewed.financemanager.events.AccountCreationEvent;
import de.homebrewed.financemanager.events.FinancialTransactionCreationEvent;
import de.homebrewed.financemanager.external.persistance.entity.AccountEntity;
import de.homebrewed.financemanager.external.persistance.entity.FinancialTransactionEntity;
import de.homebrewed.financemanager.external.persistance.repository.AccountRepository;
import de.homebrewed.financemanager.external.persistance.repository.FinancialTransactionRepository;
import de.homebrewed.financemanager.sharedmands.CreateAccountCommand;
import de.homebrewed.financemanager.sharedmands.CreateTransactionCommand;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import .junit.jupiter.api.Test;
import .springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
import .springframework.modulith.test.ApplicationModuleTest;
import .springframework.modulith.test.Scenario;
import .springframework.test.context.ActiveProfiles;

@ActiveProfiles("test")
@ApplicationModuleTest(ApplicationModuleTest.BootstrapMode.DIRECT_DEPENDENCIES)
@AutoConfigureWebMvc
@RequiredArgsConstructor
class TransactionWorkflowModuleTest {

  private final AccountRepository accountRepository;
  private final FinancialTransactionRepository financialTransactionRepository;

  @Test
  void createAccount(Scenario scenario) {
    // 1st Creating an account (usually via REST)
    var uuidAccount = UUID.randomUUID().toString();
    BigDecimal initialBalance = new BigDecimal("10000");
    CreateAccountCommand createAccountCommand =
        new CreateAccountCommand("test checking account", initialBalance);
    scenario
        .publish(() -> new AccountCreationEvent(uuidAccount, createAccountCommand))
        .andWaitForStateChange(accountRepository::findAll)
        .andVerify(
            result -> {
              assertThat(result).hasSize(1);
              AccountEntity actualAccountEntity = result.getFirst();
              assertThat(actualAccountEntity.getBalance()).isEqualTo(initialBalance);
              assertThat(actualAccountEntity.getId()).isNotNull();
            });

    // 2nd Creating a transaction to change account (subtract or add) balance
    var uuidTransaction = UUID.randomUUID().toString();
    BigDecimal amountToPay = new BigDecimal("100");
    CreateTransactionCommand createTransactionCommand =
        new CreateTransactionCommand(
            "PAYMENT", amountToPay, "USD", LocalDateTime.now(), 1L, 1L, Boolean.FALSE);
    scenario
        .publish(
            () -> new FinancialTransactionCreationEvent(uuidTransaction, createTransactionCommand))
        .andWaitForStateChange(financialTransactionRepository::findAll)
        .andVerify(
            result -> {
              assertThat(result).hasSize(1);
              FinancialTransactionEntity financialTransactionEntity = result.getFirst();
              assertThat(financialTransactionEntity.getId()).isNotNull();
              assertThat(financialTransactionEntity.getCleared()).isTrue();
            });

    // If transaction was cleared successfully, then the amount of balance has changed in the created account
    assertThat(accountRepository.findAll().getFirst().getBalance())
        .isEqualTo(initialBalance.subtract(amountToPay));
  }
}

I am trying to understand how to externalize Spring Modulith application events via Kafka, but I can't get it to work properly. The problem is that the application events are not being sent via Kafka. Nothing is happening at the moment, and I am not getting any exceptions either, which makes it hard to debug since there are no clear hints on what is going wrong.

I have provided the complete Spring Boot application on GitHub: https://github/xFakEdx/finance-manager

In src/main/resources/environment, you will find two Docker Compose scripts — one for Kafka and one for MariaDB. The application starts correctly, and via REST API, you can create an account and perform financial transactions. However, no events are being published to Kafka.

Has anyone encountered a similar issue, or does anyone have any hints or advice on what I might be missing? Any help would be greatly appreciated!


I added this ApplicationModuleTest to make clear what should happen in the workflow.

package de.homebrewed.financemanager.workflow;

import static .assertj.core.api.Assertions.assertThat;

import de.homebrewed.financemanager.events.AccountCreationEvent;
import de.homebrewed.financemanager.events.FinancialTransactionCreationEvent;
import de.homebrewed.financemanager.external.persistance.entity.AccountEntity;
import de.homebrewed.financemanager.external.persistance.entity.FinancialTransactionEntity;
import de.homebrewed.financemanager.external.persistance.repository.AccountRepository;
import de.homebrewed.financemanager.external.persistance.repository.FinancialTransactionRepository;
import de.homebrewed.financemanager.sharedmands.CreateAccountCommand;
import de.homebrewed.financemanager.sharedmands.CreateTransactionCommand;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import .junit.jupiter.api.Test;
import .springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
import .springframework.modulith.test.ApplicationModuleTest;
import .springframework.modulith.test.Scenario;
import .springframework.test.context.ActiveProfiles;

@ActiveProfiles("test")
@ApplicationModuleTest(ApplicationModuleTest.BootstrapMode.DIRECT_DEPENDENCIES)
@AutoConfigureWebMvc
@RequiredArgsConstructor
class TransactionWorkflowModuleTest {

  private final AccountRepository accountRepository;
  private final FinancialTransactionRepository financialTransactionRepository;

  @Test
  void createAccount(Scenario scenario) {
    // 1st Creating an account (usually via REST)
    var uuidAccount = UUID.randomUUID().toString();
    BigDecimal initialBalance = new BigDecimal("10000");
    CreateAccountCommand createAccountCommand =
        new CreateAccountCommand("test checking account", initialBalance);
    scenario
        .publish(() -> new AccountCreationEvent(uuidAccount, createAccountCommand))
        .andWaitForStateChange(accountRepository::findAll)
        .andVerify(
            result -> {
              assertThat(result).hasSize(1);
              AccountEntity actualAccountEntity = result.getFirst();
              assertThat(actualAccountEntity.getBalance()).isEqualTo(initialBalance);
              assertThat(actualAccountEntity.getId()).isNotNull();
            });

    // 2nd Creating a transaction to change account (subtract or add) balance
    var uuidTransaction = UUID.randomUUID().toString();
    BigDecimal amountToPay = new BigDecimal("100");
    CreateTransactionCommand createTransactionCommand =
        new CreateTransactionCommand(
            "PAYMENT", amountToPay, "USD", LocalDateTime.now(), 1L, 1L, Boolean.FALSE);
    scenario
        .publish(
            () -> new FinancialTransactionCreationEvent(uuidTransaction, createTransactionCommand))
        .andWaitForStateChange(financialTransactionRepository::findAll)
        .andVerify(
            result -> {
              assertThat(result).hasSize(1);
              FinancialTransactionEntity financialTransactionEntity = result.getFirst();
              assertThat(financialTransactionEntity.getId()).isNotNull();
              assertThat(financialTransactionEntity.getCleared()).isTrue();
            });

    // If transaction was cleared successfully, then the amount of balance has changed in the created account
    assertThat(accountRepository.findAll().getFirst().getBalance())
        .isEqualTo(initialBalance.subtract(amountToPay));
  }
}
Share Improve this question edited Mar 29 at 18:04 marc_s 756k184 gold badges1.4k silver badges1.5k bronze badges asked Mar 23 at 11:20 F4k3dF4k3d 7251 gold badge12 silver badges29 bronze badges 3
  • Would you mind altering the sample to easily execute the use case you claim doesn't work? A ./mvnw spring-boot:run fails with infrastructure missing. Ideally, you add a test case that triggers functionality ending in an event externalization using the Scenario API documented here. Setting the log level for Modulith to DEBUG should at least show the event externalization getting activated. – Oliver Drotbohm Commented Mar 23 at 16:48
  • 1 Sorry, that is a good idea. I have fotten to provide a description in the readme.md. I did it right now. I will provide a test tomorrow. – F4k3d Commented Mar 23 at 18:33
  • 1 Ok, I have added a Test to make clear what should happen – F4k3d Commented Mar 26 at 20:17
Add a comment  | 

1 Answer 1

Reset to default 2

The test of the project fail for the following reasons:

  1. You have an @ComponentScan on your application class which disables all automatic component scanning set up for the base application package. Thus only code in financemanager.external is bootstrapped, nothing else. This explains why the event listeners in your workflow package are not even getting triggered.
  2. Event externalization is only triggered for the auto-configuration packages currently, configured. For @ApplicationModuleTest executions, this means that only events from modules included in the bootstrap are externalized. As you have all events grouped in a dedicated module not included in the test run, they do not get externalized. Configuring extraIncludes = "events" does the trick here. I've filed a ticket to improve the logging at bootstrap a bit so that cases like this are easier to detect.

General recommendations

You're kind of abusing module base packages for technical decomposition. Spring Modulith does not work that way by default. Try to use business module packages (accounting, transactions?) on the top level. Further decompose into nested packages if necessary (hint: in 90% of the cases, it's not, just leads to scattered “one class per package” hard to understand codebases).

Your domains are intermingled. AccountService and FinancialTransactionService cross-reference each other's lower-level abstractions, which basically turns them into one big blob logically. I'm obviously not in the details but I wonder if we're actually talking about one domain here, not two.

The command to event listener translation feels like technical cargoculting. For the account creation you receive a request, you forward that to a service, that publishes an event, that an event listener receives, which in turn calls the actual AccountService. IMO that's way too much technical machinery and boilerplate code required before anything useful happens. Just think about how much code you would have to add to implement a new use case that starts with receiving an HTTP request. Instead, receive the call in the controller (primary adapter), delegate to the AccountService (PrimaryPort / Application), publish events to notify others about the outcome, let them react to that. Leave all the eventually-consistent event machinery to the integration between business modules.

TransactionalEventListeners should really be executed asynchronously as they otherwise block the original thread and prevent the connection of the original transaction from being released.

Finally, the ….andWaitForStateChange(accountRepository::findAll) would need to become .andWaitForStateChange(accountRepository::findAll, it -> !it.isEmpty()) as it's currently just checking for a non-null result by default and would immediately fail afterwards instead of waiting for the event-induced proceessing to finish. I filed a ticket to improve that.

Original analysis

The events published by the controllers are not emitted within a transaction. The event listener considering the @Externalized annotations on the event types is a @TransactionalEventListener, which means that it's only triggered on a transaction commit. In other words, you should get this to work by annotating the controllers with @Transactional or by introducing a transactional service you delegate the command handling to.

Events are not a separate module. They're usually the API of a module (some might even stay internal).

I've filed a ticket to potentially detect that problem and inform users more proactively about it.

本文标签: Why is event externalization not triggered in a Spring Modulith applicationStack Overflow