DDD Approach in Microservices-Based Abixen
This article will walk you through an example of a microservice-based architecture set up with a Domain Driven Design approach.
Join the DZone community and get the full member experience.
Join For FreeIn IT projects, it is extremely important to provide a clear approach when it comes up to providing easy-to-understand code, structure, and logic (generally speaking, the rules of the program). There are many specialized patterns that programmers can use, depending on the use case. In today's world, dominated by systems written in the architecture of microservices, the fundamental seems to be Domain-Driven Design.
There are many examples on the internet on the basis of two or three classes in different programming languages. However, it is difficult to find a complete, ready-to-use application written in Spring Framework. As an example, we will go through Abixen Platform. Abixen Platform is a microservices-based software platform for building enterprise applications with ready to use business intelligence and web content microservices. The source code is available on GitHub.
The purpose of this article is not to describe the theoretical basis of Domain-Driven Design. The essence is to focus on a popular application (though new to the market), with an active community on GitHub. It should also be noted here that in the current version, the project does not yet implement all the elements and features of Domain-Driven Design. For example, it has been planned that Command Query Responsibility Segregation (CQRS) will appear in the future. On the other hand, enough to present in this article and show how the implementation of DDD can look like. One of the advantages is that it's written in a popular language like Java. In addition, it's based on the commonly used Spring Boot.
In Abixen Platform, we are able to distinguish three independent domains:
- Core
- Business Intelligence
- Web Content
DDD Tiers in Abixen Platform
Since the project is a microservices-based application, the relevant statement will be one domain closed in one microservice. With extra curiosities, the domains correspond with each other on a Spring Cloud and Netflix OSS basis. At the same time, it is worth emphasizing that there are technical microservices that do not play the role of domains.
Abixen Platform assumes four tiers for a microservice:
- Interfaces
- Application
- Domain
- Infrastructure
Below is an architecture diagram of the above assumptions:
Interfaces
The interfaces tier is the tier through which the application has contact with the outside world. For example, a Core microservice has two categories of such interfaces:
- Web
- Amqp
Web
Web interfaces are responsible for communication with the application’s users. In the project, they have been implemented as Spring controllers. In the below listing is RoleController:
@Slf4j
@RestController
@RequestMapping(value = "/api/control-panel/roles")
public class RoleController {
private final RoleManagementService roleManagementService;
@Autowired
public RoleController(RoleManagementService roleManagementService) {
this.roleManagementService = roleManagementService;
}
@RequestMapping(value = "", method = RequestMethod.GET)
public Page<RoleDto> findAll(@PageableDefault(size = 1, page = 0) Pageable pageable, RoleSearchForm roleSearchForm) {
log.debug("findAll() - roleSearchForm: {}", roleSearchForm);
return roleManagementService.findAllRoles(pageable, roleSearchForm);
}
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public RoleDto find(@PathVariable Long id) {
log.debug("find() - id: {}", id);
return roleManagementService.findRole(id);
}
@RequestMapping(value = "", method = RequestMethod.POST)
public FormValidationResultDto<RoleForm> create(@RequestBody @Valid RoleForm roleForm, BindingResult bindingResult) {
log.debug("create() - roleForm: {}", roleForm);
if (bindingResult.hasErrors()) {
List<FormErrorDto> formErrors = ValidationUtil.extractFormErrors(bindingResult);
return new FormValidationResultDto<>(roleForm, formErrors);
}
final RoleForm createdRoleForm = roleManagementService.createRole(roleForm);
return new FormValidationResultDto<>(createdRoleForm);
}
@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public FormValidationResultDto<RoleForm> update(@PathVariable("id") Long id, @RequestBody @Valid RoleForm roleForm, BindingResult bindingResult) {
log.debug("update() - id: {}, roleForm: {}", id, roleForm);
if (bindingResult.hasErrors()) {
List<FormErrorDto> formErrors = ValidationUtil.extractFormErrors(bindingResult);
return new FormValidationResultDto<>(roleForm, formErrors);
}
final RoleForm updatedRoleForm = roleManagementService.updateRole(roleForm);
return new FormValidationResultDto<>(updatedRoleForm);
}
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
public ResponseEntity<Boolean> delete(@PathVariable("id") long id) {
log.debug("delete() - id: {}", id);
roleManagementService.deleteRole(id);
return new ResponseEntity<>(Boolean.TRUE, HttpStatus.OK);
}
@RequestMapping(value = "/{id}/permissions", method = RequestMethod.GET)
public RolePermissionsForm findPermissions(@PathVariable Long id) {
log.debug("findPermissions() - id: {}", id);
return roleManagementService.findRolePermissions(id);
}
@RequestMapping(value = "/{id}/permissions", method = RequestMethod.PUT)
public FormValidationResultDto<RolePermissionsForm> updatePermissions(@PathVariable("id") Long id, @RequestBody @Valid RolePermissionsForm rolePermissionsForm, BindingResult bindingResult) {
log.debug("updatePermissions() - id: {}, rolePermissionsForm: {}", id, rolePermissionsForm);
if (bindingResult.hasErrors()) {
List<FormErrorDto> formErrors = ValidationUtil.extractFormErrors(bindingResult);
return new FormValidationResultDto<>(rolePermissionsForm, formErrors);
}
final RolePermissionsForm updatedRolePermissionsForm = roleManagementService.updateRolePermissions(rolePermissionsForm);
return new FormValidationResultDto<>(updatedRolePermissionsForm);
}
}
The controller exposes a RESTful API for operations like find, findAll, create, update, delete. What is important here, the controllers have to operate on DTO classes. RoleDto acts as a representation of the domain model. RoleForm is an equivalent of command when, for example, the user creates a role. The form objects send all the information from the frontend to the application tier through the interfaces one:
@Getter
@Setter
@Accessors(chain = true)
@ToString
@EqualsAndHashCode(of = "name")
public class RoleDto extends AuditingDto {
private Long id;
private RoleType roleType;
private String name;
private Set<PermissionDto> permissions;
}
public class RoleForm implements Form {
private Long id;
@NotNull
@Length(max = Role.ROLE_NAME_MAX_LENGTH)
private String name;
private RoleType roleType;
public RoleForm() {
}
public RoleForm(Role role) {
this.id = role.getId();
this.name = role.getName();
this.roleType = role.getRoleType();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public RoleType getRoleType() {
return roleType;
}
public void setRoleType(RoleType roleType) {
this.roleType = roleType;
}
}
Amqp
Amqp interfaces are points of contacts with queues. Abixen Platform uses RabbitMQ for asynchronous communication between particular microservices. One of the use cases is deleting modules. The Core microservice acts as an orchestrator and keeps root information about registered module instances in the platform dashboard (like Business Intelligence, Web Content, or other custom modules prepared by developers). When a user executes a command to delete the module's instance, Core does a core operation and sends the command to the queue it to the Business Intelligence microservice, “Hey, I’ve just deleted my part. Now it’s your turn to remove your details of the module’s instance.” A good practice is to keep an interface and implementation there. Thanks to this, the implementation does not interfere with other tiers and they will not be aware of the queue in the system. The above Abixen Platform is realized through Spring Cloud Stream; below are listings of the interface DeleteModuleService and an implementation of AmqpDeleteModuleService:
public interface DeleteModuleService {
void delete(String routingKey, DeleteModuleCommand deleteModuleCommand);
}
@Slf4j
@Service
@EnableBinding(DeleteModuleSource.class)
public class AmqpDeleteModuleService implements DeleteModuleService {
private final DeleteModuleSource deleteModuleSource;
@Autowired
public AmqpDeleteModuleService(DeleteModuleSource deleteModuleSource) {
this.deleteModuleSource = deleteModuleSource;
}
@Override
public void delete(final String routingKey, final DeleteModuleCommand deleteModuleCommand) {
log.info("will send {}", deleteModuleCommand);
try {
final boolean sent = deleteModuleSource.output().send(
MessageBuilder.withPayload(deleteModuleCommand)
.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, routingKey)
.build());
log.info("sent {} {}", sent, deleteModuleCommand);
} catch (final Exception e) {
log.error("Couldn't send command ", e);
}
}
}
The communication process is realized by Spring Cloud Stream like so:
Application
The next tier of DDD adopted in Abixen Platform is the application. This tier is responsible mainly for the realization of requests from the interfaces tier. It is a kind of orchestrator of services sewn into the domain layer (which we will discuss later in this article). The application layer has no complicated logic; it is rather clear in this area. What is also important alsois that it operates on both domain objects and interfaces objects. The key artifacts in the application tier are services. In Abixen Platform, they are implemented as Spring Framework services. Below is a listing of DashboardService responsible for dashboard management:
@Slf4j
@Transactional
@PlatformApplicationService
public class DashboardService {
private final PageService pageService;
private final LayoutService layoutService;
private final DashboardModuleService dashboardModuleService;
private final PageToPageDtoConverter pageToPageDtoConverter;
private final ModuleToDashboardModuleDtoConverter moduleToDashboardModuleDtoConverter;
@Autowired
public DashboardService(PageService pageService,
LayoutService layoutService,
DashboardModuleService dashboardModuleService,
PageToPageDtoConverter pageToPageDtoConverter,
ModuleToDashboardModuleDtoConverter moduleToDashboardModuleDtoConverter) {
this.pageService = pageService;
this.layoutService = layoutService;
this.dashboardModuleService = dashboardModuleService;
this.pageToPageDtoConverter = pageToPageDtoConverter;
this.moduleToDashboardModuleDtoConverter = moduleToDashboardModuleDtoConverter;
}
public DashboardDto find(final Long pageId) {
log.debug("find() - pageId: {}", pageId);
final Page page = pageService.find(pageId);
final List<Module> modules = dashboardModuleService.findAllModules(page);
final PageDto pageDto = pageToPageDtoConverter.convert(page);
final List<DashboardModuleDto> dashboardModules = moduleToDashboardModuleDtoConverter.convertToList(modules);
return new DashboardDto(pageDto, dashboardModules);
}
public DashboardForm create(final DashboardForm dashboardForm) {
log.debug("create() - dashboardForm: {}", dashboardForm);
final Page page = Page.builder()
.layout(layoutService.find(dashboardForm.getPage().getLayout().getId()))
.title(dashboardForm.getPage().getTitle())
.description(dashboardForm.getPage().getDescription())
.icon(dashboardForm.getPage().getIcon())
.build();
final Page createdPage = pageService.create(page);
final PageDto pageDto = pageToPageDtoConverter.convert(createdPage);
return new DashboardForm(pageDto);
}
public DashboardForm update(final DashboardForm dashboardForm) {
log.debug("update() - dashboardForm: {}", dashboardForm);
return change(dashboardForm, false);
}
public DashboardForm configure(final DashboardForm dashboardForm) {
log.debug("configure() - dashboardForm: {}", dashboardForm);
return change(dashboardForm, true);
}
private DashboardForm change(final DashboardForm dashboardForm, final boolean configurationChangeType) {
final Page page = pageService.find(dashboardForm.getPage().getId());
if (configurationChangeType) {
validateConfiguration(dashboardForm, page);
}
page.changeDescription(dashboardForm.getPage().getDescription());
page.changeTitle(dashboardForm.getPage().getTitle());
page.changeIcon(dashboardForm.getPage().getIcon());
page.changeLayout(layoutService.find(dashboardForm.getPage().getLayout().getId()));
pageService.update(page);
final List<Long> updatedModulesIds = dashboardModuleService.updateExistingModules(dashboardForm.getDashboardModuleDtos());
final List<Long> createdModulesIds = dashboardModuleService.createNotExistingModules(dashboardForm.getDashboardModuleDtos(), page);
final List<Long> currentModulesIds = new ArrayList<>();
currentModulesIds.addAll(updatedModulesIds);
currentModulesIds.addAll(createdModulesIds);
dashboardModuleService.deleteAllModulesExcept(page, currentModulesIds);
return dashboardForm;
}
private void validateConfiguration(final DashboardForm dashboardForm, final Page page) {
boolean validationFailed = false;
if (page.getDescription() == null && dashboardForm.getPage().getDescription() != null) {
validationFailed = true;
} else if (page.getDescription() != null && !page.getDescription().equals(dashboardForm.getPage().getDescription())) {
validationFailed = true;
} else if (!page.getTitle().equals(dashboardForm.getPage().getTitle())) {
validationFailed = true;
}
if (validationFailed) {
throw new PlatformCoreException("Can not modify page's parameters during configuration's update operation.");
}
}
}
The above code shows, the application service orchestrates works of other depended services and uses converters to transform objects between users’ objects and domains’ ones and vice versa. The platform has a pattern of converters. Below is ModuleToDashboardModuleDtoConverter using dependent another one:
@Component
public class ModuleToDashboardModuleDtoConverter extends AbstractConverter<Module, DashboardModuleDto> {
private final ModuleTypeToModuleTypeDtoConverter moduleTypeToModuleTypeDtoConverter;
@Autowired
public ModuleToDashboardModuleDtoConverter(ModuleTypeToModuleTypeDtoConverter moduleTypeToModuleTypeDtoConverter) {
this.moduleTypeToModuleTypeDtoConverter = moduleTypeToModuleTypeDtoConverter;
}
@Override
public DashboardModuleDto convert(Module module, Map<String, Object> parameters) {
DashboardModuleDto dashboardModuleDto = new DashboardModuleDto();
dashboardModuleDto
.setId(module.getId())
.setTitle(module.getTitle())
.setDescription(module.getDescription())
.setType(module.getModuleType().getName())
.setModuleType(moduleTypeToModuleTypeDtoConverter.convert(module.getModuleType()))
.setRowIndex(module.getRowIndex())
.setColumnIndex(module.getColumnIndex())
.setOrderIndex(module.getOrderIndex());
return dashboardModuleDto;
}
}
Domain
The domain tier in the project is responsible for all the business operations of the microservice and can be called the heart of the system. The crucial assumptions and artifacts for this place made by Abixen Platform are as follows:
- Model
- Repositories
- Services
Model
Since the domain’s model is used by the other two artifacts, it is worth discussing the domain from this area. Abixen Platform comes up with an approach of creation models in a transparent and easy to maintain manner. For now, the project distinguishes the following building blocks:
- Entity
- Aggregate root
- Value object
Below are AclClass as the value object and Role as the entity:
@Entity
@Table(name = "acl_class")
public class AclClass {
/**
*
*/
private static final long serialVersionUID = -3518427281918839763L;
/**
* Represents a canonical name of domain class.
* E.g. com.abixen.platform.core.domain.model.User
*/
@Enumerated(EnumType.STRING)
@Column(name = "name", nullable = false)
private AclClassName aclClassName;
AclClass() {
}
public AclClassName getAclClassName() {
return aclClassName;
}
void setAclClassName(AclClassName aclClassName) {
this.aclClassName = aclClassName;
}
}
@Entity
@Table(name = "role_")
@SequenceGenerator(sequenceName = "role_seq", name = "role_seq", allocationSize = 1)
public final class Role extends AuditingModel {
public static final int ROLE_NAME_MAX_LENGTH = 300;
private static final long serialVersionUID = -1247915702100609524L;
@Id
@Column(name = "id")
@GeneratedValue(generator = "role_seq", strategy = GenerationType.SEQUENCE)
private Long id;
@Enumerated(EnumType.STRING)
@Column(name = "role_type", nullable = false)
private RoleType roleType;
@Column(name = "name", unique = true, length = ROLE_NAME_MAX_LENGTH, nullable = false)
private String name;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "role_permission", joinColumns = {@JoinColumn(name = "role_id", nullable = false, updatable = false)}, inverseJoinColumns = {@JoinColumn(name = "permission_id", nullable = false, updatable = false)})
private Set<Permission> permissions = new HashSet<>();
private Role() {
}
private void setId(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
private void setRoleType(RoleType roleType) {
this.roleType = roleType;
}
public RoleType getRoleType() {
return roleType;
}
private void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
private void setPermissions(Set<Permission> permissions) {
this.permissions = permissions;
}
public Set<Permission> getPermissions() {
return permissions;
}
public void changeDetails(String name, RoleType type) {
setName(name);
setRoleType(type);
}
public void changePermissions(Set<Permission> permissions) {
getPermissions().clear();
getPermissions().addAll(permissions);
}
public static Builder builder() {
return new Builder();
}
public static final class Builder extends EntityBuilder<Role> {
private Builder() {
}
@Override
protected void initProduct() {
this.product = new Role();
}
public Builder name(String name) {
this.product.setName(name);
return this;
}
public Builder type(RoleType roleType) {
this.product.setRoleType(roleType);
return this;
}
}
}
By implementing entities, the project imposes a certain standard of access to the objects’ creation. The purpose of this standard is to ensure that the instance of the entity is secure.
While paying attention to the implementation of the Role class, we can see that all the fields and setters are private. Only getters have a public modifier. In addition, it has been ensured that the constructor is also private. Taking advantage of this convenience, a developer using the entity will not create an instance of the class in a different way than through the supplied builder. The entity has a static builder () method that returns the object of internally implemented Builder class. The entities’ builders in the project are created in a comfortable way, extending the EntityBuilder class from the project package.
Repositories
The repositories in Abixen Platform are based on Spring Data. It should be pointed out here that the domain cannot keep an implementation of the repository. The domain should be free from all kinds of external accesses, like access to databases. Based on Spring Data, a developer can create pure interfaces and omit an implementation. However, in a case when the implementation is desirable, let’s say custom behavior is needed. The developer has to create an interface in the domain tier, but the implementation is in the infrastructure. Below is an interface of PlatformJpaRepository located just in the domain tier:
@NoRepositoryBean
public interface PlatformJpaRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
List<T> findAll(SearchForm searchForm);
Page<T> findAll(Pageable pageable, SearchForm searchForm);
List<T> findAll(SearchForm searchForm, User user, AclClassName aclClassName, PermissionName permissionName);
Page<T> findAll(Pageable pageable, SearchForm searchForm, User user, AclClassName aclClassName, PermissionName permissionName);
List<T> findAll(User user, AclClassName aclClassName, PermissionName permissionName);
Page<T> findAll(Pageable pageable, User user, AclClassName aclClassName, PermissionName permissionName);
}
Whereas its implementation PlatformJpaRepositoryImpl has been placed in the infrastructure tier.
Services
The services in this domain have been implemented as Spring Framework services. These services can inject another one from the domain tier, as well as repositories. The role of the domain services is the encapsulation of calls to repositories and other domain services. In these services, we include logic that only applies to the domain. The domain services should only operate on domain classes. Below is an implementation of RoleService:
@Slf4j
@Transactional
@PlatformDomainService
public class RoleService {
private final RoleRepository roleRepository;
private final AclSidService aclSidService;
@Autowired
public RoleService(RoleRepository roleRepository,
AclSidService aclSidService) {
this.roleRepository = roleRepository;
this.aclSidService = aclSidService;
}
public Role find(final Long id) {
log.debug("find() - id: {}", id);
return roleRepository.findOne(id);
}
public List<Role> findAll() {
log.debug("findAll()");
return roleRepository.findAll();
}
public Page<Role> findAll(final Pageable pageable, final RoleSearchForm roleSearchForm) {
log.debug("findAll() - pageable: {}, roleSearchForm: {}", pageable, roleSearchForm);
return roleRepository.findAll(pageable, roleSearchForm);
}
public Role create(final Role role) {
log.debug("create() - role: {}", role);
Role createdRole = roleRepository.save(role);
aclSidService.create(AclSidType.ROLE, createdRole.getId());
return createdRole;
}
public Role update(final Role role) {
log.debug("update() - role: {}", role);
return roleRepository.save(role);
}
public void delete(final Long id) {
log.debug("delete() - id: {}", id);
try {
roleRepository.delete(id);
} catch (Throwable e) {
e.printStackTrace();
if (e.getCause() instanceof ConstraintViolationException) {
log.warn("The role id: {} you want to remove is assigned to users.", id);
throw new PlatformRuntimeException("The role you want to remove is assigned to users.");
} else {
throw e;
}
}
}
}
As you can see, the service depends on RoleRepository and AclSidService. Also deals with Role domain class.
Infrastructure
The infrastructure tier contains all the technicalities of a given microservice. It contains all common services and global configurations that can be used by other tiers or just by the application. For example, in the infrastructure, Abixen Platform keeps the configuration of JPA in class CoreJpaConfiguration, as follows:
@Configuration
@Import(CoreDataSourceConfiguration.class)
@EnableTransactionManagement
@EnableJpaAuditing(auditorAwareRef = "platformAuditorAware")
@EnableJpaRepositories(basePackageClasses = CoreApplication.class,
repositoryFactoryBeanClass = CoreJpaRepositoryFactoryBean.class)
public class CoreJpaConfiguration extends AbstractJpaConfiguration {
@Autowired
public CoreJpaConfiguration(DataSource dataSource, AbstractPlatformJdbcConfigurationProperties platformJdbcConfiguration) {
super(dataSource, platformJdbcConfiguration, CoreApplication.class.getPackage().getName());
}
public AuditorAware platformAuditorAware() {
return new PlatformAuditorAware();
}
}
There can also be implementation of repository interfaces like in the case of PlatformJpaRepositoryImpl:
public class PlatformJpaRepositoryImpl<T, ID extends Serializable>
extends SimpleJpaRepository<T, ID> implements PlatformJpaRepository<T, ID> {
private final EntityManager entityManager;
public PlatformJpaRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
public Page<T> findAll(Pageable pageable, SearchForm searchForm, User user, AclClassName aclClassName, PermissionName permissionName) {
Specification<T> securedSpecification = SecuredSpecifications.getSpecification(user, aclClassName, permissionName);
Specification<T> searchFormSpecification = SearchFormSpecifications.getSpecification(searchForm);
Specification<T> specification = AndSpecifications.getSpecification(searchFormSpecification, securedSpecification);
return (Page) (null == pageable ? new PageImpl(this.findAll()) : this.findAll(Specifications.where(specification), pageable));
}
public List<T> findAll(SearchForm searchForm, User user, AclClassName aclClassName, PermissionName permissionName) {
Specification<T> securedSpecification = SecuredSpecifications.getSpecification(user, aclClassName, permissionName);
Specification<T> searchFormSpecification = SearchFormSpecifications.getSpecification(searchForm);
Specification<T> specification = AndSpecifications.getSpecification(searchFormSpecification, securedSpecification);
return this.findAll(Specifications.where(specification));
}
public Page<T> findAll(Pageable pageable, User user, AclClassName aclClassName, PermissionName permissionName) {
Specification<T> securedSpecification = SecuredSpecifications.getSpecification(user, aclClassName, permissionName);
return (Page) (null == pageable ? new PageImpl(this.findAll()) : this.findAll(Specifications.where(securedSpecification), pageable));
}
public List<T> findAll(User user, AclClassName aclClassName, PermissionName permissionName) {
Specification<T> securedSpecification = SecuredSpecifications.getSpecification(user, aclClassName, permissionName);
return this.findAll(Specifications.where(securedSpecification));
}
public Page<T> findAll(Pageable pageable, SearchForm searchForm) {
Specification<T> searchFormSpecification = SearchFormSpecifications.getSpecification(searchForm);
return (Page) (null == pageable ? new PageImpl(this.findAll()) : this.findAll(Specifications.where(searchFormSpecification), pageable));
}
public List<T> findAll(SearchForm searchForm) {
Specification<T> searchFormSpecification = SearchFormSpecifications.getSpecification(searchForm);
return this.findAll(Specifications.where(searchFormSpecification));
}
}
Thanks to this, the domain is not dependent on a specific implementation of database access. The domain is not aware of this since it keeps only the interfaces.
What Next?
Please note that this project does not yet implement the entire Domain-Driven Design. What has been presented in this article is a summary of an interesting approach to the subject of DDD using technologies such as Java 8 and Spring Framework supported by examples based on the open source project. In the future, more DDD artifacts implementations will probably appear. I deeply hope that the above examples will allow you to design your own application based on DDD.
If, despite everything, you would like to share your constructive comments, I will be very pleased to take part in the discussion.
Opinions expressed by DZone contributors are their own.
Comments