Skip to content

Commit

Permalink
anton-liauchuk#11 use axon event handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
anton-liauchuk committed Jan 11, 2021
1 parent 4308ce7 commit 4aeba1f
Show file tree
Hide file tree
Showing 27 changed files with 167 additions and 166 deletions.
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,52 @@ Definition of common formats for API.
### 3.2. Communications between bounded contexts
Communication between bounded contexts is asynchronous. Bounded contexts don't share data, it's forbidden to create a transaction which spans more than one bounded context.

This solution reduces coupling of bounded contexts through data replication across contexts which results to higher bounded contexts independence.
This solution reduces coupling of bounded contexts through data replication across contexts which results to higher bounded contexts independence. Event publishing/subscribing is used from Axon Framework. The example of implementation:
```java
@RequiredArgsConstructor
@Component
public class ApproveCourseProposalCommandHandler {

private final TransactionTemplate transactionTemplate;
private final CourseProposalRepository repository;
private final EventBus eventBus;

/**
* Handles approve course proposal command. Approves and save approved course proposal
*
* @param command command
* @throws ResourceNotFoundException if resource not found
* @throws CourseProposalAlreadyApprovedException course proposal already approved
*/
@CommandHandler
@PreAuthorize("hasRole('ADMIN')")
public void handle(ApproveCourseProposalCommand command) {
final CourseProposal proposal = transactionTemplate.execute(transactionStatus -> {
// the logic related to approving the proposal inside the transaction
});

final CourseProposalDTO dto = Objects.requireNonNull(proposal).toDTO();
// publishing integration event outside the transaction
eventBus.publish(GenericEventMessage.asEventMessage(new CourseApprovedByAdminIntegrationEvent(dto.getUuid())));
}
}
```

The listener for this integration event:
```java
@Component
@RequiredArgsConstructor
public class SendCourseToApproveIntegrationEventHandler {

private final CommandGateway commandGateway;

@EventHandler
public void handleSendCourseToApproveEvent(SendCourseToApproveIntegrationEvent event) {
commandGateway.send(new CreateCourseProposalCommand(event.getCourseId()));
}

}
```

### 3.3. Validation
Always valid approach is used. So domain model will be changed from one valid state to another valid state. Technically, validation rules are defined on `Command` models and executed during processing the command. Javax validation-api is used for defining the validation rules via annotations.
Expand Down Expand Up @@ -300,7 +345,7 @@ ArchUnit are used for implementing architecture tests. These tests are placed in
**LayerTest** - tests for validating the dependencies between layers of application.

### 3.11. Axon Framework
Axon Framework is used as DDD library for not creating custom building block classes.
Axon Framework is used as DDD library for not creating custom building block classes. Also, more functionality for event publishing/event sourcing is used from Axon functionality.

### 3.12. Bounded context map
![](docs/bounded_context_map.png)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import lombok.RequiredArgsConstructor;

import org.axonframework.commandhandling.CommandHandler;
import org.springframework.context.ApplicationEventPublisher;
import org.axonframework.eventhandling.EventBus;
import org.axonframework.eventhandling.GenericEventMessage;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;
Expand All @@ -26,7 +27,7 @@ public class ApproveCourseProposalCommandHandler {

private final TransactionTemplate transactionTemplate;
private final CourseProposalRepository repository;
private final ApplicationEventPublisher eventPublisher;
private final EventBus eventBus;

/**
* Handles approve course proposal command. Approves and save approved course proposal
Expand All @@ -52,6 +53,6 @@ public void handle(ApproveCourseProposalCommand command) {
});

final CourseProposalDTO dto = Objects.requireNonNull(proposal).toDTO();
eventPublisher.publishEvent(new CourseApprovedByAdminIntegrationEvent(dto, dto.getUuid()));
eventBus.publish(GenericEventMessage.asEventMessage(new CourseApprovedByAdminIntegrationEvent(dto.getUuid())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@
import lombok.RequiredArgsConstructor;

import org.axonframework.commandhandling.gateway.CommandGateway;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.axonframework.eventhandling.EventHandler;
import org.springframework.stereotype.Component;

/**
* Event listener for {@link SendCourseToApproveIntegrationEvent}, executes the logic for creating course proposal by {@link CreateCourseProposalCommandHandler}.
*/
@Component
@RequiredArgsConstructor
public class SendCourseToApproveIntegrationEventListener {
public class SendCourseToApproveIntegrationEventHandler {

private final CommandGateway commandGateway;

@Async
@EventListener
@EventHandler
public void handleSendCourseToApproveEvent(SendCourseToApproveIntegrationEvent event) {
commandGateway.send(new CreateCourseProposalCommand(event.getCourseId()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import lombok.RequiredArgsConstructor;

import org.axonframework.commandhandling.CommandHandler;
import org.springframework.context.ApplicationEventPublisher;
import org.axonframework.eventhandling.EventBus;
import org.axonframework.eventhandling.GenericEventMessage;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;
Expand All @@ -26,7 +27,7 @@ public class DeclineCourseProposalCommandHandler {

private final TransactionTemplate transactionTemplate;
private final CourseProposalRepository repository;
private final ApplicationEventPublisher eventPublisher;
private final EventBus eventBus;

/**
* Handles decline course proposal command. Declines and save declined course proposal
Expand All @@ -52,7 +53,7 @@ public void handle(DeclineCourseProposalCommand command) {
});

final CourseProposalDTO dto = Objects.requireNonNull(proposal).toDTO();
eventPublisher.publishEvent(new CourseDeclinedByAdminIntegrationEvent(dto, dto.getUuid()));
eventBus.publish(GenericEventMessage.asEventMessage(new CourseDeclinedByAdminIntegrationEvent(dto.getUuid())));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import com.educational.platform.administration.integration.event.CourseApprovedByAdminIntegrationEvent;
import com.educational.platform.common.exception.ResourceNotFoundException;
import org.assertj.core.api.ThrowableAssert;
import org.axonframework.eventhandling.EventBus;
import org.axonframework.eventhandling.GenericEventMessage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
Expand All @@ -39,14 +40,14 @@ public class ApproveCourseProposalCommandHandlerTest {
private TransactionTemplate transactionTemplate;

@Mock
private ApplicationEventPublisher eventPublisher;
private EventBus eventBus;

private ApproveCourseProposalCommandHandler sut;

@BeforeEach
void setUp() {
transactionTemplate = new TransactionTemplate(transactionManager);
sut = new ApproveCourseProposalCommandHandler(transactionTemplate, repository, eventPublisher);
sut = new ApproveCourseProposalCommandHandler(transactionTemplate, repository, eventBus);
}

@Test
Expand All @@ -70,9 +71,9 @@ void handle_existingCourseProposal_courseProposalSavedWithStatusApproved() {
assertThat(proposal)
.hasFieldOrPropertyWithValue("status", CourseProposalStatus.APPROVED);

final ArgumentCaptor<CourseApprovedByAdminIntegrationEvent> eventArgument = ArgumentCaptor.forClass(CourseApprovedByAdminIntegrationEvent.class);
verify(eventPublisher).publishEvent(eventArgument.capture());
final CourseApprovedByAdminIntegrationEvent event = eventArgument.getValue();
final ArgumentCaptor<GenericEventMessage<CourseApprovedByAdminIntegrationEvent>> eventArgument = ArgumentCaptor.forClass(GenericEventMessage.class);
verify(eventBus).publish(eventArgument.capture());
final CourseApprovedByAdminIntegrationEvent event = eventArgument.getValue().getPayload();
assertThat(event)
.hasFieldOrPropertyWithValue("courseId", uuid);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@
import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
public class SendCourseToApproveIntegrationEventListenerTest {
public class SendCourseToApproveIntegrationEventHandlerTest {

@Mock
private CommandGateway commandGateway;

@InjectMocks
private SendCourseToApproveIntegrationEventListener sut;
private SendCourseToApproveIntegrationEventHandler sut;

@Test
void handleCourseApprovedByAdminEvent_approveCourseCommandExecuted() {
// given
final UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426655440001");
final SendCourseToApproveIntegrationEvent event = new SendCourseToApproveIntegrationEvent(new Object(), uuid);
final SendCourseToApproveIntegrationEvent event = new SendCourseToApproveIntegrationEvent(uuid);

// when
sut.handleSendCourseToApproveEvent(event);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
import com.educational.platform.administration.course.CourseProposalRepository;
import com.educational.platform.administration.course.CourseProposalStatus;
import com.educational.platform.administration.course.create.CreateCourseProposalCommand;
import com.educational.platform.administration.integration.event.CourseDeclinedByAdminIntegrationEvent;
import com.educational.platform.common.exception.ResourceNotFoundException;
import org.assertj.core.api.ThrowableAssert;
import org.axonframework.eventhandling.EventBus;
import org.axonframework.eventhandling.GenericEventMessage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
Expand All @@ -39,14 +39,14 @@ public class DeclineCourseProposalCommandHandlerTest {
private TransactionTemplate transactionTemplate;

@Mock
private ApplicationEventPublisher eventPublisher;
private EventBus eventBus;

private DeclineCourseProposalCommandHandler sut;

@BeforeEach
void setUp() {
transactionTemplate = new TransactionTemplate(transactionManager);
sut = new DeclineCourseProposalCommandHandler(transactionTemplate, repository, eventPublisher);
sut = new DeclineCourseProposalCommandHandler(transactionTemplate, repository, eventBus);
}

@Test
Expand All @@ -70,9 +70,9 @@ void handle_existingCourseProposal_courseProposalSavedWithStatusDeclined() {
assertThat(proposal)
.hasFieldOrPropertyWithValue("status", CourseProposalStatus.DECLINED);

final ArgumentCaptor<CourseDeclinedByAdminIntegrationEvent> eventArgument = ArgumentCaptor.forClass(CourseDeclinedByAdminIntegrationEvent.class);
verify(eventPublisher).publishEvent(eventArgument.capture());
final CourseDeclinedByAdminIntegrationEvent event = eventArgument.getValue();
var eventArgument = ArgumentCaptor.forClass(GenericEventMessage.class);
verify(eventBus).publish(eventArgument.capture());
var event = eventArgument.getValue().getPayload();
assertThat(event)
.hasFieldOrPropertyWithValue("courseId", uuid);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package com.educational.platform.administration.integration.event;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

import java.util.UUID;

/**
* Represents course approved by admin integration event, should be published after approval the course by admin.
*/
@Getter
public class CourseApprovedByAdminIntegrationEvent extends ApplicationEvent {
public class CourseApprovedByAdminIntegrationEvent {


private final UUID courseId;
Expand All @@ -18,12 +17,9 @@ public class CourseApprovedByAdminIntegrationEvent extends ApplicationEvent {
/**
* Create a new {@code CourseApprovedByAdminIntegrationEvent}.
*
* @param source the object on which the event initially occurred or with
* which the event is associated (never {@code null})
* @param courseId course id
*/
public CourseApprovedByAdminIntegrationEvent(Object source, UUID courseId) {
super(source);
public CourseApprovedByAdminIntegrationEvent(UUID courseId) {
this.courseId = courseId;
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package com.educational.platform.administration.integration.event;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

import java.util.UUID;

/**
* Represents course declined by admin integration event.
*/
@Getter
public class CourseDeclinedByAdminIntegrationEvent extends ApplicationEvent {
public class CourseDeclinedByAdminIntegrationEvent {


private final UUID courseId;
Expand All @@ -18,12 +17,9 @@ public class CourseDeclinedByAdminIntegrationEvent extends ApplicationEvent {
/**
* Create a new {@code CourseDeclinedByAdminIntegrationEvent}.
*
* @param source the object on which the event initially occurred or with
* which the event is associated (never {@code null})
* @param courseId course id
*/
public CourseDeclinedByAdminIntegrationEvent(Object source, UUID courseId) {
super(source);
public CourseDeclinedByAdminIntegrationEvent(UUID courseId) {
this.courseId = courseId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import lombok.RequiredArgsConstructor;

import org.axonframework.commandhandling.CommandHandler;
import org.springframework.context.ApplicationEventPublisher;
import org.axonframework.eventhandling.EventBus;
import org.axonframework.eventhandling.GenericEventMessage;
import org.springframework.lang.NonNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
Expand All @@ -29,8 +30,8 @@ public class RegisterStudentToCourseCommandHandler {
private final TransactionTemplate transactionTemplate;
private final CourseEnrollmentRepository courseEnrollmentRepository;
private final CourseEnrollmentFactory courseEnrollmentFactory;
private final ApplicationEventPublisher eventPublisher;
private final CurrentUserAsStudent currentUserAsStudent;
private final EventBus eventBus;

/**
* Creates course enrollment from command.
Expand All @@ -49,11 +50,8 @@ public UUID handle(RegisterStudentToCourseCommand command) {
});

final UUID uuid = Objects.requireNonNull(courseEnrollment).getUuid();

eventPublisher.publishEvent(
new StudentEnrolledToCourseIntegrationEvent(courseEnrollment,
command.getCourseId(),
currentUserAsStudent.userAsStudent().toReference()));
eventBus.publish(GenericEventMessage.asEventMessage(new StudentEnrolledToCourseIntegrationEvent(command.getCourseId(),
currentUserAsStudent.userAsStudent().toReference())));

return uuid;
}
Expand Down
Loading

0 comments on commit 4aeba1f

Please sign in to comment.