Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Modular Monolith Java application with DDD

License

NotificationsYou must be signed in to change notification settings

anton-liauchuk/educational-platform

Repository files navigation

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 inhttps://github.com/anton-liauchuk/educational-platform/issues

High-level plan is represented in the table

FeatureStatus
Modular monolith with base functionalityCOMPLETED
Microservices
UI application

3. Architecture

3.1. Module structure

Modules which represent business logic:

administration

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

courses

ATeacher can create newCourse. ThisCourse can be edited. AStudent can view the list ofCourse and search by different parameters. TheCourse contains the list ofLecture. After getting the approval,Course can be published. Depends on other modules, number of students and course rating can be recalculated.

course-enrollments

AStudent can enrollCourse. ALecture can be marked as completed.Student can view the list ofCourse 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

AStudent can create/edit feedback to enrolledCourse. The list ofCourse Review are used for calculating the rating ofCourse andTeacher.Course Review contains comment and rating.

users

AUser can be created after registration.User has the list ofPermission.User can edit info in profile.User has roleStudent after registration.User can become aTeacher. 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:

@RequiredArgsConstructor@ComponentpublicclassApproveCourseProposalCommandHandler {privatefinalTransactionTemplatetransactionTemplate;privatefinalCourseProposalRepositoryrepository;privatefinalEventBuseventBus;/**     * 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')")publicvoidhandle(ApproveCourseProposalCommandcommand) {finalCourseProposalproposal =transactionTemplate.execute(transactionStatus -> {// the logic related to approving the proposal inside the transaction        });finalCourseProposalDTOdto =Objects.requireNonNull(proposal).toDTO();// publishing integration event outside the transactioneventBus.publish(GenericEventMessage.asEventMessage(newCourseApprovedByAdminIntegrationEvent(dto.getUuid())));    }}

The listener for this integration event:

@Component@RequiredArgsConstructorpublicclassSendCourseToApproveIntegrationEventHandler {privatefinalCommandGatewaycommandGateway;@EventHandlerpublicvoidhandleSendCourseToApproveEvent(SendCourseToApproveIntegrationEventevent) {commandGateway.send(newCreateCourseProposalCommand(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 onCommand 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:

/** * Create course command. */@Builder@Data@AllArgsConstructorpublicclassCreateCourseCommand {@NotBlankprivatefinalStringname;@NotBlankprivatefinalStringdescription;}

Example of running validation rules inside the factory:

/** * Represents Course Factory. */@RequiredArgsConstructor@ComponentpublicclassCourseFactory {privatefinalValidatorvalidator;privatefinalCurrentUserAsTeachercurrentUserAsTeacher;/** * Creates course from command. * * @param courseCommand course command * @return course * @throws ConstraintViolationException in the case of validation issues */publicCoursecreateFrom(CreateCourseCommandcourseCommand) {finalSet<ConstraintViolation<CreateCourseCommand>>violations =validator.validate(courseCommand);if (!violations.isEmpty()) {thrownewConstraintViolationException(violations);}varteacher =currentUserAsTeacher.userAsTeacher();returnnewCourse(courseCommand,teacher.getId());}}

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:

/** * Represents Course API adapter. */@Validated@RequestMapping(value ="/courses")@RestController@RequiredArgsConstructorpublicclassCourseController {privatefinalCommandGatewaycommandGateway;@PostMapping(consumes =APPLICATION_JSON_VALUE,produces =APPLICATION_JSON_VALUE)@ResponseStatus(HttpStatus.CREATED)CreatedCourseResponsecreate(@Valid@RequestBodyCreateCourseRequestcourseCreateRequest) {finalCreateCourseCommandcommand =CreateCourseCommand.builder()                .name(courseCreateRequest.getName())                .description(courseCreateRequest.getDescription())                .build();returnnewCreatedCourseResponse(commandGateway.sendAndWait(command));    }//...}

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:

@RestControllerAdvicepublicclassGlobalExceptionHandler {@ExceptionHandler(MethodArgumentNotValidException.class)publicResponseEntity<ErrorResponse>onMethodArgumentNotValidException(MethodArgumentNotValidExceptione) {varerrors =e.getBindingResult().getFieldErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList());returnResponseEntity.status(HttpStatus.BAD_REQUEST).body(newErrorResponse(errors));}//...}

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:

@RequiredArgsConstructor@Component@TransactionalpublicclassPublishCourseCommandHandler {privatefinalCourseRepositoryrepository;/**     * 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)")publicvoidhandle(@P("c")PublishCourseCommandcommand) {finalOptional<Course>dbResult =repository.findByUuid(command.getUuid());if (dbResult.isEmpty()) {thrownewResourceNotFoundException(String.format("Course with uuid: %s not found",command.getUuid()));        }finalCoursecourse =dbResult.get();course.publish();repository.save(course);    }}

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

/** * Represents course repository. */publicinterfaceCourseRepositoryextendsJpaRepository<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")UUIDuuid);//...}

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 insidedocs/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 insideconfiguration 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.

5. Useful links

  • 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 underMIT license.


[8]ページ先頭

©2009-2025 Movatter.jp