Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,10 @@ curl -X PUT "http://localhost:8080/random-users/1" \
PostgreSQL
```

### Domain Service Ports
### Service Architecture

- `LocalUserService`: local persistence operations (save, read, delete) backed by PostgreSQL.
- `RemoteUserService`: external user source contract used by use cases.
- `UserService`: domain port for local persistence operations (save, read, delete) backed by PostgreSQL. Implemented by `UserServiceImpl` in the data layer.
- `RemoteUserService`: data-layer interface for external API adapters. Implemented by `DummyUserServiceImpl` and `RandomUserServiceImpl`.

### External Source Adapter

Expand Down
2 changes: 1 addition & 1 deletion mvnw.cmd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<testcontainers.version>2.0.4</testcontainers.version>
<skipDocker>false</skipDocker>
<sonar.coverage.exclusions>**/*Application.java</sonar.coverage.exclusions>
<sonar.test.exclusions>**/feature/SpringIntegrationTest.java</sonar.test.exclusions>
<sonar.test.exclusions>**/features/SpringIntegrationTest.java</sonar.test.exclusions>
<sonar.coverage.jacoco.xmlReportPaths>${project.build.directory}/site/jacoco/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
</properties>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,40 @@

import com.xpeho.spring_boot_java_random_user.data.models.api.dummy.DummyUserResultDTO;
import com.xpeho.spring_boot_java_random_user.data.models.api.randomuser.RandomUserResultDTO;
import com.xpeho.spring_boot_java_random_user.data.models.database.User;
import com.xpeho.spring_boot_java_random_user.data.models.database.UserDAO;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import org.springframework.stereotype.Service;


@Service
public class UserConverter {
// Domain -> DAO
public User toDao(UserEntity entity) {
User user = new User();
user.setId(entity.id());
user.setGender(entity.gender());
user.setFirstname(entity.firstname());
user.setLastname(entity.lastname());
user.setCivility(entity.civility());
user.setEmail(entity.email());
user.setPhone(entity.phone());
user.setPicture(entity.picture());
user.setNationality(entity.nat());
return user;
public UserDAO toDao(UserEntity entity) {
UserDAO userDAO = new UserDAO();
userDAO.setId(entity.id());
userDAO.setGender(entity.gender());
userDAO.setFirstname(entity.firstname());
userDAO.setLastname(entity.lastname());
userDAO.setCivility(entity.civility());
userDAO.setEmail(entity.email());
userDAO.setPhone(entity.phone());
userDAO.setPicture(entity.picture());
userDAO.setNationality(entity.nat());
return userDAO;
}

// DAO -> Domain
public UserEntity toDomain(User user) {
public UserEntity toDomain(UserDAO userDAO) {
return new UserEntity(
user.getId(),
user.getGender(),
user.getFirstname(),
user.getLastname(),
user.getCivility(),
user.getEmail(),
user.getPhone(),
user.getPicture(),
user.getNationality()
userDAO.getId(),
userDAO.getGender(),
userDAO.getFirstname(),
userDAO.getLastname(),
userDAO.getCivility(),
userDAO.getEmail(),
userDAO.getPhone(),
userDAO.getPicture(),
userDAO.getNationality()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

@Entity
@Table(name = "users")
public class User {
public class UserDAO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Expand All @@ -32,7 +32,7 @@ public class User {
private String nationality;

// Required by JPA
public User() {
public UserDAO() {
// No initialization needed
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
package com.xpeho.spring_boot_java_random_user.data.services;

import com.xpeho.spring_boot_java_random_user.data.converters.UserConverter;
import com.xpeho.spring_boot_java_random_user.data.models.database.User;
import com.xpeho.spring_boot_java_random_user.data.models.database.UserDAO;
import com.xpeho.spring_boot_java_random_user.data.sources.api.RemoteUserService;
import com.xpeho.spring_boot_java_random_user.data.sources.database.UserRepository;
import com.xpeho.spring_boot_java_random_user.data.sources.database.UserSpecifications;
import com.xpeho.spring_boot_java_random_user.domain.entities.PaginatedUsers;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserFilter;
import com.xpeho.spring_boot_java_random_user.domain.services.LocalUserService;
import com.xpeho.spring_boot_java_random_user.domain.enums.UserSource;
import com.xpeho.spring_boot_java_random_user.domain.services.UserService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

@Service

public class UserServiceImpl implements LocalUserService {
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserConverter userConverter;
private final Map<UserSource, RemoteUserService> remoteUserServices;

public UserServiceImpl(UserRepository userRepository, UserConverter userConverter) {
public UserServiceImpl(
UserRepository userRepository,
UserConverter userConverter,
List<RemoteUserService> remoteUserServices
) {
this.userRepository = userRepository;
this.userConverter = userConverter;
this.remoteUserServices = remoteUserServices.stream()
.collect(Collectors.toMap(RemoteUserService::getSource, Function.identity()));
}

@Override
public List<UserEntity> saveAll(List<UserEntity> users) {
List<User> daoUsers = users.stream().map(userConverter::toDao).toList();
Iterable<User> saved = userRepository.saveAll(daoUsers);
List<UserDAO> userDAOs = users.stream().map(userConverter::toDao).toList();
Iterable<UserDAO> saved = userRepository.saveAll(userDAOs);
return StreamSupport.stream(saved.spliterator(), false)
.map(userConverter::toDomain)
.toList();
Expand All @@ -41,8 +56,8 @@ public Optional<UserEntity> getById(long id) {

@Override
public UserEntity save(UserEntity user) {
User savedUser = userRepository.save(userConverter.toDao(user));
return userConverter.toDomain(savedUser);
UserDAO savedUserDAO = userRepository.save(userConverter.toDao(user));
return userConverter.toDomain(savedUserDAO);
}

@Override
Expand All @@ -51,9 +66,19 @@ public void deleteById(long id) {
}

@Override
public List<UserEntity> filterUsers(UserFilter filter) {
return userRepository.findAll(UserSpecifications.byFilter(filter)).stream()
.map(userConverter::toDomain)
.toList();
public Page<UserEntity> filterUsers(UserFilter filter, Pageable pageable) {
return userRepository.findAll(UserSpecifications.byFilter(filter), pageable)
.map(userConverter::toDomain);
}

@Override
public PaginatedUsers fetchAndSaveUsers(int page, int size, UserSource source) throws IOException {
RemoteUserService remoteService = remoteUserServices.get(source);
if (remoteService == null) {
throw new IllegalStateException("No remote service configured for source: " + source);
}
PaginatedUsers response = remoteService.fetchUsers(page, size);
saveAll(response.data());
return response;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.xpeho.spring_boot_java_random_user.domain.services;
package com.xpeho.spring_boot_java_random_user.data.sources.api;

import com.xpeho.spring_boot_java_random_user.domain.entities.PaginatedUsers;
import com.xpeho.spring_boot_java_random_user.domain.enums.UserSource;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.xpeho.spring_boot_java_random_user.data.sources.api;

import com.xpeho.spring_boot_java_random_user.data.sources.api.dummy.DummyUserApi;
import com.xpeho.spring_boot_java_random_user.data.sources.api.randomuser.RandomUserApi;
import jakarta.annotation.PreDestroy;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

@Configuration
public class UserApiConfig {

private OkHttpClient okHttpClient;

/**
* Single shared OkHttpClient intentionally reused by both Retrofit instances (dummyUserRetrofit and randomUserRetrofit).
* Sharing a single client allows both APIs to benefit from a common connection pool, thread pool,
* and keep-alive settings, reducing resource consumption.
* If the two APIs ever require distinct timeouts or interceptors, separate clients should be created.
* The {@link jakarta.annotation.PreDestroy} hook ensures the client is shut down cleanly on application stop.
*/
@Bean
public OkHttpClient okHttpClient() {
okHttpClient = new OkHttpClient.Builder().build();
return okHttpClient;
}

@Bean(name = "dummyUserRetrofit")
public Retrofit dummyUserRetrofit(OkHttpClient okHttpClient, Environment env) {
return new Retrofit.Builder()
.baseUrl(env.getRequiredProperty("dummy.api.base-url"))
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build();
}

@Bean
public DummyUserApi dummyUserApi(@Qualifier("dummyUserRetrofit") Retrofit dummyUserRetrofit) {
return dummyUserRetrofit.create(DummyUserApi.class);
}

@Bean(name = "randomUserRetrofit")
public Retrofit randomUserRetrofit(OkHttpClient okHttpClient, Environment env) {
return new Retrofit.Builder()
.baseUrl(env.getRequiredProperty("randomuser.api.base-url"))
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build();
}

@Bean
public RandomUserApi randomUserApi(@Qualifier("randomUserRetrofit") Retrofit randomUserRetrofit) {
return randomUserRetrofit.create(RandomUserApi.class);
}

@PreDestroy
public void destroy() {
if (okHttpClient != null) {
okHttpClient.dispatcher().executorService().shutdown();
okHttpClient.connectionPool().evictAll();
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import com.xpeho.spring_boot_java_random_user.domain.entities.PaginatedUsers;
import com.xpeho.spring_boot_java_random_user.domain.enums.UserSource;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import com.xpeho.spring_boot_java_random_user.domain.services.RemoteUserService;
import com.xpeho.spring_boot_java_random_user.data.sources.api.RemoteUserService;
import org.springframework.stereotype.Service;
import retrofit2.Response;

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import com.xpeho.spring_boot_java_random_user.domain.entities.PaginatedUsers;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
import com.xpeho.spring_boot_java_random_user.domain.enums.UserSource;
import com.xpeho.spring_boot_java_random_user.domain.services.RemoteUserService;
import com.xpeho.spring_boot_java_random_user.data.sources.api.RemoteUserService;
import org.springframework.stereotype.Service;
import retrofit2.Response;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.xpeho.spring_boot_java_random_user.data.sources.database;

import com.xpeho.spring_boot_java_random_user.data.models.database.User;
import com.xpeho.spring_boot_java_random_user.data.models.database.UserDAO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
public interface UserRepository extends JpaRepository<UserDAO, Long>, JpaSpecificationExecutor<UserDAO> {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.xpeho.spring_boot_java_random_user.data.sources.database;

import com.xpeho.spring_boot_java_random_user.data.models.database.User;
import com.xpeho.spring_boot_java_random_user.data.models.database.UserDAO;
import com.xpeho.spring_boot_java_random_user.domain.entities.UserFilter;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Path;
Expand All @@ -15,7 +15,7 @@ public final class UserSpecifications {
private UserSpecifications() {
}

public static Specification<User> byFilter(UserFilter filter) {
public static Specification<UserDAO> byFilter(UserFilter filter) {
return (user, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(long id) {
super("User not found with id: " + id);
super("UserDAO not found with id: " + id);
Comment thread
MayuriXx marked this conversation as resolved.
}
}
Loading
Loading