Example of a modular monolith with DDD

Posted by

Intro

I got an idea to implement a modular monolith with DDD using Java stack and was targeting the following results:

  • the modular monolith with DDD implementation;
  • separation of bounded contexts;
  • example of communications between bounded contexts;
  • example of simple CQRS implementation;
  • documentation of architecture decisions;
  • best practice/patterns using;

Main functionality

This application https://github.com/anton-liauchuk/educational-platform represents an educational platform for online learning. The functionality is similar to Udemy: teachers can publish courses and lectures and students can enroll in different courses and leave their feedback. The integration events schema represents full functionality:

Module structure

Business logic modules:

administration

Administrator can approve or decline a CourseProposal. After approving or declining a proposal, the corresponding integration event is being published to other modules.

courses

A Teacher can create a new Course. This Course can be edited. A Student can view the list of Course and search by different parameters. The Course contains a list of Lecture. After getting the approval, Course can be published. A number of students and the course rating calculation and updates depend on other modules.

course-enrollments

A Student can enroll in a Course. A Lecture can be marked as completed. Student can view the list of Course Enrollment. Course Enrollment can be archived, completed. On a new enrollment action, the number of students is recalculated and a new number is published as an integration event.

course-reviews

A Student can create/edit feedback on the enrolled Course. The list of Course Review is used for calculation of ratings of  Course and Teacher. Course Review contains comments and the rating. (either the rating if calculated value or ratings if it is a list of them)

users

A User can be created after the registration. User has a list of Permission. User can edit their profile info. User has the Student role after the registration. User can become a Teacher. After the registration,  a new user integration event is being published to other modules.

Each business module has 3 sub-modules:

application

Contains the domain model, application services and other logic related to the main functionality of the module.

integration-events

Integration events that  can be published from this business module.

web

API implementation.

Base technical functionality modules:

common

Contains common functionality that  can be used in other modules.

configuration

The module contains the start application logic for initializing application context, therefore this module has dependency on all other modules. Architecture tests are being placed in the test folder.

security

Contains the security related logic.

web

The definition of the API common formats: 

Technology stack

Technology stack used for the application:

  • Spring;
  • Java 11;
  • Lombok;
  • Axon Framework;
  • ArchUnit;
  • Gradle;

Communication between bounded contexts

Communication between bounded contexts is asynchronous. Bounded contexts don’t share data, it’s forbidden to create a transaction for multiple bounded contexts.

This solution reduces the coupling of bounded contexts through data replication across contexts resulting in higher bounded contexts independence. Event publishing/subscribing is used from Axon Framework. The  implementation example:

@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:

@Component
@RequiredArgsConstructor
public class SendCourseToApproveIntegrationEventHandler {

    private final CommandGateway commandGateway;

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

Architectural test examples

ArchUnit is used for implementing architecture tests. These tests are located in the configuration module because  it has  dependencies on all other modules of the application. It makes the configuration module the best place for storing the tests to validate the full code base of the application.

IntegrationEventTest – tests for the integration events format validation.

CommandHandlerTest – tests for the command handlers and related classes validation.

LayerTest – tests for the application layers dependencies validation.

Simple CQRS implementation

CQRS principle gives the flexibility in optimizing models for read and write operations. The simple version of CQRS is implemented in the application. On write operations, the full logic is executed via aggregate. On read operations, DTO objects are created via JPQL queries on the repository level. A command handler example:

@RequiredArgsConstructor
@Component
@Transactional
public class PublishCourseCommandHandler {

    private final CourseRepository repository;

    /**
     * Handles publish course command. Publishes and save published course
     *
     * @param command command
     * @throws ResourceNotFoundException        if resource not found
     * @throws CourseCannotBePublishedException if course is not approved
     */
    @CommandHandler
    @PreAuthorize("hasRole('TEACHER') and @courseTeacherChecker.hasAccess(authentication, #c.uuid)")
    public void handle(@P("c") PublishCourseCommand command) {
        final Optional<Course> dbResult = repository.findByUuid(command.getUuid());
        if (dbResult.isEmpty()) {
            throw new ResourceNotFoundException(String.format("Course with uuid: %s not found", command.getUuid()));
        }

        final Course course = dbResult.get();
        course.publish();
        repository.save(course);
    }
}

Example of a query implementation constructing DTO object inside Spring repository:

/**
 * Represents course repository.
 */
public interface CourseRepository extends JpaRepository<Course, Integer> {

	/**
	 * Retrieves a course dto by its uuid.
	 *
	 * @param uuid must not be {@literal null}.
	 * @return the course dto with the given uuid or {@literal Optional#empty()} if none found.
	 * @throws IllegalArgumentException if {@literal uuid} is {@literal null}.
	 */
	@Query(value = "SELECT new com.educational.platform.courses.course.CourseDTO(c.uuid, c.name, c.description, c.numberOfStudents) "
			+ "FROM com.educational.platform.courses.course.Course c WHERE c.uuid = :uuid")
	Optional<CourseDTO> findDTOByUuid(@Param("uuid") UUID uuid);

	//...
}

Always valid approach

Domain model is changed from one valid state to another valid state. Technically, validation rules are defined on Command models and are being executed during the command processing. Javax validation-api is used to define the validation rules through annotations.

Example of validation rules for command:

/**
 * Create course command.
 */
@Builder
@Data
@AllArgsConstructor
public class CreateCourseCommand {

    @NotBlank
    private final String name;

    @NotBlank
    private final String description;

}

Example of running validation rules inside the factory:

/**
 * Represents Course Factory.
 */
@RequiredArgsConstructor
@Component
public class CourseFactory {

	private final Validator validator;
	private final CurrentUserAsTeacher currentUserAsTeacher;

	/**
	 * Creates course from command.
	 *
	 * @param courseCommand course command
	 * @return course
	 * @throws ConstraintViolationException in the case of validation issues
	 */
	public Course createFrom(CreateCourseCommand courseCommand) {
		final Set<ConstraintViolation<CreateCourseCommand>> violations = validator.validate(courseCommand);
		if (!violations.isEmpty()) {
			throw new ConstraintViolationException(violations);
		}

		var teacher = currentUserAsTeacher.userAsTeacher();
		return new Course(courseCommand, teacher.getId());
	}

}

Command handlers/factories contain a complete set of validation rules. Also, some format validation can be executed in the controller. It is needed for a fail-fast solution and to prepare the messages with http request context. Example of a running format validation:

/**
 * Represents Course API adapter.
 */
@Validated
@RequestMapping(value = "/courses")
@RestController
@RequiredArgsConstructor
public class CourseController {

    private final CommandGateway commandGateway;

    @PostMapping(consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
    @ResponseStatus(HttpStatus.CREATED)
    CreatedCourseResponse create(@Valid @RequestBody CreateCourseRequest courseCreateRequest) {
        final CreateCourseCommand command = CreateCourseCommand.builder()
                .name(courseCreateRequest.getName())
                .description(courseCreateRequest.getDescription())
                .build();

        return new CreatedCourseResponse(commandGateway.sendAndWait(command));
    }
    
    //...
}

In Spring Framework, this validation works by @Valid and @Validated annotations. As a result, in the case of validation errors, we should handle the MethodArgumentNotValidException exception. The error handling logic is represented in the GlobalExceptionHandler:

@RestControllerAdvice
public class GlobalExceptionHandler {

	@ExceptionHandler(MethodArgumentNotValidException.class)
	public ResponseEntity<ErrorResponse> onMethodArgumentNotValidException(MethodArgumentNotValidException e) {
		var errors = e.getBindingResult()
				.getFieldErrors()
				.stream()
				.map(DefaultMessageSourceResolvable::getDefaultMessage)
				.collect(Collectors.toList());

		return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(errors));
	}
	//...
}

Conclusion

In this article, I shared my experience with writing a modular monolith with DDD using Java stack. Please find the full code and documentation and provide your feedback on technical solutions at https://github.com/anton-liauchuk/educational-platform.  The application is in its development state, please feel free to submit a pull request or create an issue.

2 comments

  1. Hi Anton.
    First of all… Great work!
    Im using Postman trying to reach content from usersController but I can’t achieve it.
    How could I reach an endpoint for the “educational-platform”?
    Is there any security token that should be provided so this could be achieved?

    1. hi
      thank you!
      please sign-up:

      POST http://127.0.0.1:8080/users/sign-up
      Content-Type: application/json

      {
      "role": "ROLE_STUDENT",
      "username": "student",
      "email": "111@gmail.com",
      "password": "11111111"
      }

      After that, you can use the token from the response, an example:

      GET http://127.0.0.1:8080/course-enrollments
      Authorization: Bearer TOKEN

Leave a Reply