- Notifications
You must be signed in to change notification settings - Fork70
Modular Monolith Java application with DDD
License
anton-liauchuk/educational-platform
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
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
- 2. Plan
- 3. Architecture
- 3.1. Module structure
- 3.2. Communications between bounded contexts
- 3.3. Validation
- 3.4. CQRS
- 3.5. The identifiers for communication between modules
- 3.6. API First
- 3.7. Rich Domain Model
- 3.8. Architecture Decisions
- 3.9. Results from command handlers
- 3.10. Architecture tests
- 3.11. Axon Framework
- 3.12. Bounded context map
- 3.13. Integration events inside application
- 3.14. Technology stack
- 4. Contribution
- 5. Useful links
- 6. License
- 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;
The issues are represented inhttps://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 |
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.
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())); }}
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));}//...}
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);//...}
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.
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
Rich domain model solution is used. Domain model encapsulates internal structure and logic.
All decisions inside this project are placed insidedocs/architecture-decisions.
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.
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.
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.
- Spring;
- Java 21;
- Lombok;
- Axon Framework;
- ArchUnit;
- Gradle;
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.
The project is underMIT license.