Gateway Layer: Bridging the Data Access
The gateway layer is responsible for interfacing with the data storage mechanisms, typically a database, and transforming the retrieved data into data transfer objects (DTOs). This ensures that our use case layer remains clean and focused on business logic without being polluted by data access concerns.
Interface Definition
For each gateway, we define an interface to abstract the data access operations. Note this interface will belong in the use case layer. The implementation will live the the gateway layer.
public interface GetUserAccountGateway {
Optional<UserAccountGatewayDto> execute(String accountId) throws GatewayException;
}
Implementation Example
Let's look at an implementation example of the gateway interface, which accesses the data, retrieves the entity, and transforms it into a DTO using a mapper.
package com.example.datahub.gateway;
import com.example.datahub.gateway.data.entity.UserAccountEntity;
import com.example.datahub.gateway.data.repository.UserAccountRepository;
import com.example.datahub.usecase.gateway.GetUserAccountGateway;
import com.example.datahub.usecase.gateway.dto.UserAccountGatewayDto;
import com.example.datahub.usecase.gateway.exception.GatewayException;
import java.util.Optional;
import java.util.function.Function;
public class GetUserAccountGatewayImpl implements GetUserAccountGateway {
private final Function<UserAccountEntity, UserAccountGatewayDto> userAccountEntityToDtoMapper;
private final UserAccountRepository userAccountRepository;
public GetUserAccountGatewayImpl(
Function<UserAccountEntity, UserAccountGatewayDto> userAccountEntityToDtoMapper,
UserAccountRepository userAccountRepository) {
this.userAccountEntityToDtoMapper = userAccountEntityToDtoMapper;
this.userAccountRepository = userAccountRepository;
}
@Override
public Optional<UserAccountGatewayDto> execute(String accountId) throws GatewayException {
try {
return userAccountRepository
.findById(accountId)
.map(userAccountEntityToDtoMapper);
} catch (RuntimeException exception) {
throw new GatewayException(exception);
}
}
}
Key Points
- Data Access and Transformation:
- The gateway implementation accesses the data repository to retrieve an entity.
- It uses a mapper to transform the entity into a DTO, ensuring that the rest of the application only deals with DTOs, maintaining a clean separation of concerns.
- Repository Interface:
- The repository interface extends
CrudRepository
, providing basic CRUD operations. - We selected
CrudRepository
for its simplicity and built-in support for standard data access operations, which aligns with our clean code principles.
- The repository interface extends
package com.example.datahub.gateway.data.repository;
import com.example.datahub.gateway.data.entity.UserAccountEntity;
import java.util.Optional;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.repository.CrudRepository;
public interface UserAccountRepository extends CrudRepository<UserAccountEntity, String> {
@EntityGraph(value = "UserAccountEntity.details")
@Override
Optional<UserAccountEntity> findById(String id);
}
Entity Definition:
- The entity class represents the database table structure and is annotated with JPA annotations for ORM (Object-Relational Mapping).
package com.example.datahub.gateway.data.entity;
import jakarta.persistence.*;
import java.util.LinkedHashSet;
import java.util.Set;
@Entity
@Table(name = "user_account")
public class UserAccountEntity {
@Id
private String accountId;
private String username;
private String email;
// Getters and setters...
}
Mapper Class:
- The mapper class is responsible for converting the entity to a DTO, adhering to our principle of not instantiating objects directly within our code.
- Interesting part about mappers. I like to hand write mine, however it is possible to use Map Struct or Lombok to do this for you automagically.
package com.example.datahub.gateway.mapper;
import com.example.datahub.gateway.data.entity.UserAccountEntity;
import com.example.datahub.usecase.gateway.dto.UserAccountGatewayDto;
import com.example.datahub.usecase.gateway.dto.builder.UserAccountGatewayDtoBuilder;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class UserAccountEntityToDtoMapper implements Function<UserAccountEntity, UserAccountGatewayDto> {
private final Supplier<UserAccountGatewayDtoBuilder> userAccountGatewayDtoBuilder;
private final BiConsumer<UserAccountEntity, UserAccountGatewayDtoBuilder> userAccountEntityToDtoMapper;
public UserAccountEntityToDtoMapper(
BiConsumer<UserAccountEntity, UserAccountGatewayDtoBuilder> userAccountEntityToDtoMapper,
Supplier<UserAccountGatewayDtoBuilder> userAccountGatewayDtoBuilder) {
this.userAccountEntityToDtoMapper = userAccountEntityToDtoMapper;
this.userAccountGatewayDtoBuilder = userAccountGatewayDtoBuilder;
}
@Override
public UserAccountGatewayDto apply(UserAccountEntity userAccountEntity) {
UserAccountGatewayDtoBuilder builder = userAccountGatewayDtoBuilder.get();
userAccountEntityToDtoMapper.accept(userAccountEntity, builder);
return builder.build();
}
}
Conclusion
The gateway layer is essential for maintaining a clean architecture by isolating the data access logic from the business logic. By using repositories, entities, and mappers, we ensure that our use case layer remains focused and uncluttered by data access concerns. In future posts, we'll continue to explore other layers of our architecture and how we apply clean code principles to each.