项目作者: anton-liauchuk

项目描述 :
Modular Monolith Java application with DDD
高级语言: Java
项目地址: git://github.com/anton-liauchuk/educational-platform.git
创建时间: 2019-10-06T17:51:58Z
项目社区:https://github.com/anton-liauchuk/educational-platform

开源协议:MIT License

下载


Educational platform

Example of Modular Monolith Java application with DDD. In the plans, this application will be moved to microservices architecture.

1. The goals of this application

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

2. Plan

The issues are represented in https://github.com/anton-liauchuk/educational-platform/issues

High-level plan is represented in the table

Feature Status
Modular monolith with base functionality COMPLETED
Microservices
UI application

3. Architecture

3.1. Module structure

Modules which represent business logic:

administration

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

courses

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

course-enrollments

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

course-reviews

A Student can create/edit feedback to enrolled Course. The list of Course Review are used for calculating the rating of Course and Teacher. Course Review contains comment and rating.

users

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

Each business module has 3 sub-modules:

application

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

integration-events

Integration events which can be published from this business module.

web

API implementation.

Modules with base technical functionality:

common

Contains common functionality which can be used in other modules.

configuration

Module contains start application logic for initializing application context, it’s why this module has dependency to all other modules. Architecture tests are placed inside test folder.

security

Contains the logic related to security.

web

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. Event publishing/subscribing is used from Axon Framework. The example of implementation:

  1. @RequiredArgsConstructor
  2. @Component
  3. public class ApproveCourseProposalCommandHandler {
  4. private final TransactionTemplate transactionTemplate;
  5. private final CourseProposalRepository repository;
  6. private final EventBus eventBus;
  7. /**
  8. * Handles approve course proposal command. Approves and save approved course proposal
  9. *
  10. * @param command command
  11. * @throws ResourceNotFoundException if resource not found
  12. * @throws CourseProposalAlreadyApprovedException course proposal already approved
  13. */
  14. @CommandHandler
  15. @PreAuthorize("hasRole('ADMIN')")
  16. public void handle(ApproveCourseProposalCommand command) {
  17. final CourseProposal proposal = transactionTemplate.execute(transactionStatus -> {
  18. // the logic related to approving the proposal inside the transaction
  19. });
  20. final CourseProposalDTO dto = Objects.requireNonNull(proposal).toDTO();
  21. // publishing integration event outside the transaction
  22. eventBus.publish(GenericEventMessage.asEventMessage(new CourseApprovedByAdminIntegrationEvent(dto.getUuid())));
  23. }
  24. }

The listener for this integration event:

  1. @Component
  2. @RequiredArgsConstructor
  3. public class SendCourseToApproveIntegrationEventHandler {
  4. private final CommandGateway commandGateway;
  5. @EventHandler
  6. public void handleSendCourseToApproveEvent(SendCourseToApproveIntegrationEvent event) {
  7. commandGateway.send(new CreateCourseProposalCommand(event.getCourseId()));
  8. }
  9. }

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.

Example of validation rules for command:

  1. /**
  2. * Create course command.
  3. */
  4. @Builder
  5. @Data
  6. @AllArgsConstructor
  7. public class CreateCourseCommand {
  8. @NotBlank
  9. private final String name;
  10. @NotBlank
  11. private final String description;
  12. }

Example of running validation rules inside the factory:

  1. /**
  2. * Represents Course Factory.
  3. */
  4. @RequiredArgsConstructor
  5. @Component
  6. public class CourseFactory {
  7. private final Validator validator;
  8. private final CurrentUserAsTeacher currentUserAsTeacher;
  9. /**
  10. * Creates course from command.
  11. *
  12. * @param courseCommand course command
  13. * @return course
  14. * @throws ConstraintViolationException in the case of validation issues
  15. */
  16. public Course createFrom(CreateCourseCommand courseCommand) {
  17. final Set<ConstraintViolation<CreateCourseCommand>> violations = validator.validate(courseCommand);
  18. if (!violations.isEmpty()) {
  19. throw new ConstraintViolationException(violations);
  20. }
  21. var teacher = currentUserAsTeacher.userAsTeacher();
  22. return new Course(courseCommand, teacher.getId());
  23. }
  24. }

Command handlers/factories contain complete rules of validation. Also, some format validation can be executed inside the controller. It’s needed for fail-fast solution and for preparing the messages with context of http request.
Example of running format validation:

  1. /**
  2. * Represents Course API adapter.
  3. */
  4. @Validated
  5. @RequestMapping(value = "/courses")
  6. @RestController
  7. @RequiredArgsConstructor
  8. public class CourseController {
  9. private final CommandGateway commandGateway;
  10. @PostMapping(consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
  11. @ResponseStatus(HttpStatus.CREATED)
  12. CreatedCourseResponse create(@Valid @RequestBody CreateCourseRequest courseCreateRequest) {
  13. final CreateCourseCommand command = CreateCourseCommand.builder()
  14. .name(courseCreateRequest.getName())
  15. .description(courseCreateRequest.getDescription())
  16. .build();
  17. return new CreatedCourseResponse(commandGateway.sendAndWait(command));
  18. }
  19. //...
  20. }

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

  1. @RestControllerAdvice
  2. public class GlobalExceptionHandler {
  3. @ExceptionHandler(MethodArgumentNotValidException.class)
  4. public ResponseEntity<ErrorResponse> onMethodArgumentNotValidException(MethodArgumentNotValidException e) {
  5. var errors = e.getBindingResult()
  6. .getFieldErrors()
  7. .stream()
  8. .map(DefaultMessageSourceResolvable::getDefaultMessage)
  9. .collect(Collectors.toList());
  10. return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(errors));
  11. }
  12. //...
  13. }

3.4. CQRS

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

  1. @RequiredArgsConstructor
  2. @Component
  3. @Transactional
  4. public class PublishCourseCommandHandler {
  5. private final CourseRepository repository;
  6. /**
  7. * Handles publish course command. Publishes and save published course
  8. *
  9. * @param command command
  10. * @throws ResourceNotFoundException if resource not found
  11. * @throws CourseCannotBePublishedException if course is not approved
  12. */
  13. @CommandHandler
  14. @PreAuthorize("hasRole('TEACHER') and @courseTeacherChecker.hasAccess(authentication, #c.uuid)")
  15. public void handle(@P("c") PublishCourseCommand command) {
  16. final Optional<Course> dbResult = repository.findByUuid(command.getUuid());
  17. if (dbResult.isEmpty()) {
  18. throw new ResourceNotFoundException(String.format("Course with uuid: %s not found", command.getUuid()));
  19. }
  20. final Course course = dbResult.get();
  21. course.publish();
  22. repository.save(course);
  23. }
  24. }

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

  1. /**
  2. * Represents course repository.
  3. */
  4. public interface CourseRepository extends JpaRepository<Course, Integer> {
  5. /**
  6. * Retrieves a course dto by its uuid.
  7. *
  8. * @param uuid must not be {@literal null}.
  9. * @return the course dto with the given uuid or {@literal Optional#empty()} if none found.
  10. * @throws IllegalArgumentException if {@literal uuid} is {@literal null}.
  11. */
  12. @Query(value = "SELECT new com.educational.platform.courses.course.CourseDTO(c.uuid, c.name, c.description, c.numberOfStudents) "
  13. + "FROM com.educational.platform.courses.course.Course c WHERE c.uuid = :uuid")
  14. Optional<CourseDTO> findDTOByUuid(@Param("uuid") UUID uuid);
  15. //...
  16. }

3.5. The identifiers for communication between modules

Natural keys or uuids are used as identifiers. Primary keys are forbidden for communications between modules or with external systems. If entity has good natural key - it’s the most preferable choice for identifier between modules.

3.6. API First

API First is one of engineering and architecture principles. In a nutshell API First requires two aspects:

  • define APIs first, before coding its implementation, using a standard specification language;
  • get early review feedback from peers and client developers;

By defining APIs outside the code, we want to facilitate early review feedback and also a development discipline that focus service interface design on:

  • profound understanding of the domain and required functionality
  • generalized business entities / resources, i.e. avoidance of use case specific APIs
  • clear separation of WHAT vs. HOW concerns, i.e. abstraction from implementation aspects — APIs should be stable even if we replace complete service implementation including its underlying technology stack

3.7. Rich Domain Model

Rich domain model solution is used. Domain model encapsulates internal structure and logic.

3.8. Architecture Decisions

All decisions inside this project are placed inside docs/architecture-decisions.

3.9. Results from command handlers

The idea from CQRS - do not return anything from command processing. But in some cases, we need to get generated identifiers of new created resources. So as trade-off, command handlers can return generated identifiers after processing if it’s needed.

3.10. Architecture tests

ArchUnit are used for implementing architecture tests. These tests are placed inside configuration module because this module has the dependencies to all other modules inside the application. It means that it’s the best place for storing the tests which should validate full code base inside application.

IntegrationEventTest - tests for validating the format of integration events.

CommandHandlerTest - tests for validating the command handlers and related classes.

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. Also, more functionality for event publishing/event sourcing is used from Axon functionality.

3.12. Bounded context map

3.13. Integration events inside application

3.14. Technology stack

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

4. Contribution

The application is in development status. Please feel free to submit pull request or create the issue.

  • Modular monolith with DDD - the most influenced project. This project was started as attempt of implementing something similar with Java stack.
  • Knowledge base - The knowledge base about Java, DDD and other topics.

6. License

The project is under MIT license.