Mastering Microservices: Implemenatation of User Service

Mastering Microservices: Implemenatation of User Service

Β·

12 min read

πŸš€ Welcome to our Blog on Implementing the User Service in a Spring Boot Microservice Banking Application! πŸŒπŸ’Ό

Discover the core elements and insights that shape user management in our microservices-driven banking system. Whether you're a developer or a tech enthusiast, join us for practical tips and a deep dive into building a robust User Service.

So what are the functionalities that we are gonna build?

  • User Registration: Enroll users seamlessly into the banking system. πŸ“₯🌐

  • Reading All Users: Retrieve a complete list of registered users. πŸ“‹πŸ‘₯

  • Reading User by ID: Access detailed user information via their unique ID. πŸ”πŸ†”

  • Reading User by Account ID: Efficiently fetch user details using their account ID. πŸ”πŸ’³

  • Update User Status: Modify user details promptly based on user requests. πŸ”„πŸ‘€

Development

Now, move to your favorite IDE or to the Spring Initializer and create a Spring boot Application with the following dependencies

Now, what are we gonna use?

Model Layer

Entities

The entity model is represented using the Java Persistence API (JPA) annotations, and it follows a relational database model. Let's break down the database model aspects based on the entities in the code:

User.java

package org.training.user.service.model.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.training.user.service.model.Status;

import javax.persistence.*;
import java.time.LocalDate;

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    private String emailId;

    private String contactNo;

    private String authId;

    private String identificationNumber;

    @CreationTimestamp
    private LocalDate creationOn;

    @Enumerated(EnumType.STRING)
    private Status status;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "user_profile_id", referencedColumnName = "userProfileId")
    private UserProfile userProfile;
}

User Entity:

  • Purpose: Security

    • The User entity primarily focuses on managing information related to user authentication and security.

    • Fields such as emailId, contactNo, authId, and identificationNumber are indicative of security-related attributes.

    • It includes a status field, denoting the status of the user (e.g., PENDING, APPROVED, DISABLED, REJECTED), which could be crucial for access control and account management.

    • The userProfileId establishes a one-to-one relationship with the UserProfile entity, allowing for the separation of security-related data from personal information.

UserProfile.java

package org.training.user.service.model.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserProfile {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userProfileId;

    private String firstName;

    private String lastName;

    private String gender;

    private String address;

    private String occupation;

    private String martialStatus;

    private String nationality;
}

UserProfile Entity:

  • Purpose: Personal Information Storage

    • The UserProfile entity is designed to store personal information about a user.

    • Fields like firstName, lastName, gender, address, occupation, martialStatus, and nationality is indicative of personal details.

    • This separation of concerns (security-related information in User and personal information in UserProfile) is a common practice to enhance security and maintain a clean and organized database structure.

    • It allows for flexibility and scalability, as personal information can be managed independently from security-related data.

DTO, Mappers, REST responses and requests

DTOs streamline data exchange, reducing over-fetching or under-fetching. Mappers facilitate seamless translation between layers, enhancing modularity. REST responses and requests ensure standardized, predictable client-server interactions, supporting statelessness. This synergy optimizes data flow, promotes security, and adheres to RESTful principles for efficient communication in applications.

CreateUser.java

package org.training.user.service.model.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CreateUser {

    private String firstName;

    private String lastName;

    private String contactNumber;

    private String emailId;

    private String password;
}

UserDto.java

package org.training.user.service.model.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.training.user.service.model.Status;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserDto {

    private Long userId;

    private String emailId;

    private String password;

    private String identificationNumber;

    private String authId;

    private Status status;

    private UserProfileDto userProfileDto;
}

UserProfileDto.java

package org.training.user.service.model.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserProfileDto {

    private String firstName;

    private String lastName;

    private String gender;

    private String address;

    private String occupation;

    private String martialStatus;

    private String nationality;
}

UserUpdate.java

package org.training.user.service.model.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserUpdate {

    private String firstName;

    private String lastName;

    private String contactNo;

    private String address;

    private String gender;

    private String occupation;

    private String martialStatus;

    private String nationality;
}

UserUpdateStatus.java

package org.training.user.service.model.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.training.user.service.model.Status;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserUpdateStatus {

    private Status status;
}

Since we frequently map Entity to DTO and vice-versa we will be introducing the BaseMapper.java which will be our abstract class, and then we will be implementing the required methods in the UserMapper.java class

BaseMapper.java

package org.training.user.service.model.mapper;

import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public abstract class BaseMapper<E, D> {

    public abstract E convertToEntity(D dto, Object... args);

    public abstract D convertToDto(E entity, Object... args);

    public Collection<E> convertToEntity(Collection<D> dto, Object... args) {
        return dto.stream().map(d -> convertToEntity(d, args)).collect(Collectors.toList());
    }

    public Collection<D> convertToDto(Collection<E> entities, Object... args) {
        return entities.stream().map(entity ->convertToDto(entity, args)).collect(Collectors.toList());
    }

    public List<E> covertToEntityList(Collection<D> dtos, Object... args) {
        return convertToEntity(dtos, args).stream().collect(Collectors.toList());
    }

    public List<D> convertToDtoList(Collection<E> entities, Object... args) {
        return convertToDto(entities, args).stream().collect(Collectors.toList());
    }

    public Set<E> convertToEntitySet(Collection<D> dtos, Object... args) {
        return convertToEntity(dtos, args).stream().collect(Collectors.toSet());
    }

    public Set<D> convertToDtoSet(Collection<E> entities, Object... args) {
        return convertToDto(entities, args).stream().collect(Collectors.toSet());
    }

}

UserMapper.java

package org.training.user.service.model.mapper;

import org.modelmapper.ModelMapper;
import org.springframework.beans.BeanUtils;
import org.training.user.service.model.dto.UserDto;
import org.training.user.service.model.dto.UserProfileDto;
import org.training.user.service.model.entity.User;
import org.training.user.service.model.entity.UserProfile;

import java.util.Objects;

public class UserMapper extends BaseMapper<User, UserDto>{

    private final ModelMapper mapper = new ModelMapper();

    @Override
    public User convertToEntity(UserDto dto, Object... args) {

        User user = new User();
        if(!Objects.isNull(dto)){
            BeanUtils.copyProperties(dto, user);
            if(!Objects.isNull(dto.getUserProfileDto())){
                UserProfile userProfile = new UserProfile();
                BeanUtils.copyProperties(dto.getUserProfileDto(), userProfile);
                user.setUserProfile(userProfile);
            }
        }
        return user;
    }

    @Override
    public UserDto convertToDto(User entity, Object... args) {

        UserDto userDto = new UserDto();
        if(!Objects.isNull(entity)){
            BeanUtils.copyProperties(entity, userDto);
            if(!Objects.isNull(entity.getUserProfile())) {
                UserProfileDto userProfileDto = new UserProfileDto();
                BeanUtils.copyProperties(entity.getUserProfile(), userProfileDto);
                userDto.setUserProfileDto(userProfileDto);
            }
        }
        return userDto;
    }
}

Repository Layer

The repository provides a clean and high-level interface for interacting with the database, abstracting away the complexities of data access. It promotes code reusability, consistency, and adherence to best practices in database operations within the context of a Spring Boot application.

UserRepository.java

package org.training.user.service.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.training.user.service.model.entity.User;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findUserByAuthId(String authId);
}

Service Implementation

We will be creating two services: one to communicate with the Keycloak instance and another to communicate with the database.

Keycloak Configuration

Let's create a Keycloak service that facilitates communication between the Keycloak instance and the user service.

  • First, we will create another client banking-service-api-client.

    Creating client

  • Then we will enble Client authentication as we did before, and also enable the Service account roles which enables you can manage the authorization and capabilities of the service accounts, ensuring they have the necessary permissions to perform specific tasks or access particular resources.

    Enabling the Service account roles

  • Finally, change the Valid redirect URL to *.

  • Next, navigate to the Service Account Roles section via the navigation bar to assign the manage-users role from the realm-management label. .

  • Now, click Assign Roles and apply filters by clients. Find and enable the manage-user role.

Now, all the required configurations for using the client to communicate with the user service and Keycloak instance are added.

For a detailed document containing all the steps and additional explanations, please refer to the comprehensive setup guide provided with the below document.

Now, let's add some configurations that will be helpful in establishing communication.

app:
  config:
    keycloak:
      server-url: http://localhost:8571
      realm: banking-service
      client-id: banking-service-api-client
      client-secret: 932OFITe8vtH4z0G9gem4Afd69JhbwrI

Now, we will create a class that will be helpful in mapping the above configuration with Java classes.

KeyCloakProperties.java

package org.training.user.service.config;

import lombok.extern.slf4j.Slf4j;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class KeyCloakProperties {

    @Value("${app.config.keycloak.server-url}")
    private String serverUrl;

    @Value("${app.config.keycloak.realm}")
    private String realm;

    @Value("${app.config.keycloak.client-id}")
    private String clientId;

    @Value("${app.config.keycloak.client-secret}")
    private String clientSecret;

    private static Keycloak keycloakInstance = null;

    public Keycloak getKeycloakInstance() {

        if (keycloakInstance == null) {
            keycloakInstance = KeycloakBuilder.builder()
                    .serverUrl(serverUrl)
                    .realm(realm)
                    .clientId(clientId)
                    .clientSecret(clientSecret)
                    .grantType("client_credentials")
                    .build();
        }

        return keycloakInstance;
    }

    public String getRealm() {
        return realm;
    }
}
  • @Slf4j: Lombok annotation for automatic generation of a logger field.

  • @Component: Indicates that this class is a Spring component, and its instance can be managed by the Spring IoC container.

  • serverUrl, realm, clientId, and clientSecret: Configuration properties for the Keycloak server connection.

  • getKeycloakInstance(): Method to retrieve or create a singleton instance of the Keycloak client.

    • Checks if the keycloakInstance is null; if yes, creates a new instance using the provided configuration properties.

    • The Keycloak instance is configured with the server URL, realm, client ID, client secret, and grant type.

KeyCloakManager.java

package org.training.user.service.config;

import lombok.RequiredArgsConstructor;
import org.keycloak.admin.client.resource.RealmResource;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class KeyCloakManager {

    private final KeyCloakProperties keyCloakProperties;

    public RealmResource getKeyCloakInstanceWithRealm() {

        return keyCloakProperties.getKeycloakInstance().realm(keyCloakProperties.getRealm());
    }
}
  • Returns a RealmResource instance for the specified Keycloak realm.

  • Calls the getKeycloakInstance() method from KeyCloakProperties to obtain the Keycloak client instance.

  • Uses the configured realm from KeyCloakProperties to retrieve the RealmResource.

Keycloak Service

KeyCloakService.java

package org.training.user.service.service;

import org.keycloak.representations.idm.UserRepresentation;

import java.util.List;

public interface KeycloakService {

    Integer createUser(UserRepresentation userRepresentation);

    List<UserRepresentation> readUserByEmail(String emailId);

    List<UserRepresentation> readUsers(List<String> authIds);

    UserRepresentation readUser(String authId);

    void updateUser(UserRepresentation userRepresentation);
}

KeycloakServiceImpl.java

package org.training.user.service.service.implementation;

import lombok.RequiredArgsConstructor;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.stereotype.Service;
import org.training.user.service.config.KeyCloakManager;
import org.training.user.service.service.KeycloakService;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class KeycloakServiceImpl implements KeycloakService {

    private final KeyCloakManager keyCloakManager;

    @Override
    public Integer createUser(UserRepresentation userRepresentation) {
        return keyCloakManager.getKeyCloakInstanceWithRealm().users().create(userRepresentation).getStatus();
    }

    @Override
    public List<UserRepresentation> readUserByEmail(String emailId) {
        return keyCloakManager.getKeyCloakInstanceWithRealm().users().search(emailId);
    }

    @Override
    public List<UserRepresentation> readUsers(List<String> authIds) {

        return authIds.stream().map(authId -> {
            UserResource usersResource = keyCloakManager.getKeyCloakInstanceWithRealm().users().get(authId);
            return usersResource.toRepresentation();
        }).collect(Collectors.toList());
    }

    @Override
    public UserRepresentation readUser(String authId) {

        UsersResource userResource = keyCloakManager.getKeyCloakInstanceWithRealm().users();
        return userResource.get(authId).toRepresentation();
    }

    @Override
    public void updateUser(UserRepresentation userRepresentation) {

        keyCloakManager.getKeyCloakInstanceWithRealm().users()
                .get(userRepresentation.getId()).update(userRepresentation);
    }
}

User Service

We will be creating different methods that help us in achieving different goals like creating user, reading users, deleting user and also updating them.

UserService.java

package org.training.user.service.service;

import org.training.user.service.model.dto.CreateUser;
import org.training.user.service.model.dto.UserDto;
import org.training.user.service.model.dto.UserUpdate;
import org.training.user.service.model.dto.UserUpdateStatus;
import org.training.user.service.model.dto.response.Response;

import java.util.List;

public interface UserService {

    Response createUser(CreateUser userDto);

    List<UserDto> readAllUsers();

    UserDto readUser(String authId);

    Response updateUserStatus(Long id, UserUpdateStatus userUpdate);

    Response updateUser(Long id, UserUpdate userUpdate);

    UserDto readUserById(Long userId);

    UserDto readUserByAccountId(String accountId);
}

The readUserByAccountId(String accountId) method will be implemented in future articles as it requires inter-service communication.

UserServiceImpl.java

package org.training.user.service.service.implementation;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.training.user.service.exception.EmptyFields;
import org.training.user.service.exception.ResourceConflictException;
import org.training.user.service.exception.ResourceNotFound;
import org.training.user.service.external.AccountService;
import org.training.user.service.model.Status;
import org.training.user.service.model.dto.CreateUser;
import org.training.user.service.model.dto.UserDto;
import org.training.user.service.model.dto.UserUpdate;
import org.training.user.service.model.dto.UserUpdateStatus;
import org.training.user.service.model.dto.response.Response;
import org.training.user.service.model.entity.User;
import org.training.user.service.model.entity.UserProfile;
import org.training.user.service.model.external.Account;
import org.training.user.service.model.mapper.UserMapper;
import org.training.user.service.repository.UserRepository;
import org.training.user.service.service.KeycloakService;
import org.training.user.service.service.UserService;
import org.training.user.service.utils.FieldChecker;

import javax.transaction.Transactional;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final KeycloakService keycloakService;
    private final AccountService accountService;

    private UserMapper userMapper = new UserMapper();

    @Value("${spring.application.success}")
    private String responseCodeSuccess;

    @Value("${spring.application.not_found}")
    private String responseCodeNotFound;

    @Override
    public Response createUser(CreateUser userDto) {

        List<UserRepresentation> userRepresentations = keycloakService.readUserByEmail(userDto.getEmailId());
        if(userRepresentations.size() > 0) {
            log.error("This emailId is already registered as a user");
            throw new ResourceConflictException("This emailId is already registered as a user");
        }

        UserRepresentation userRepresentation = new UserRepresentation();
        userRepresentation.setUsername(userDto.getEmailId());
        userRepresentation.setFirstName(userDto.getFirstName());
        userRepresentation.setLastName(userDto.getLastName());
        userRepresentation.setEmailVerified(false);
        userRepresentation.setEnabled(false);
        userRepresentation.setEmail(userDto.getEmailId());

        CredentialRepresentation credentialRepresentation = new CredentialRepresentation();
        credentialRepresentation.setValue(userDto.getPassword());
        credentialRepresentation.setTemporary(false);
        userRepresentation.setCredentials(Collections.singletonList(credentialRepresentation));

        Integer userCreationResponse = keycloakService.createUser(userRepresentation);

        if (userCreationResponse.equals(201)) {

            List<UserRepresentation> representations = keycloakService.readUserByEmail(userDto.getEmailId());
            UserProfile userProfile = UserProfile.builder()
                    .firstName(userDto.getFirstName())
                    .lastName(userDto.getLastName()).build();

            User user = User.builder()
                    .emailId(userDto.getEmailId())
                    .contactNo(userDto.getContactNumber())
                    .status(Status.PENDING).userProfile(userProfile)
                    .authId(representations.get(0).getId())
                    .identificationNumber(UUID.randomUUID().toString()).build();

            userRepository.save(user);
            return Response.builder()
                    .responseMessage("User created successfully")
                    .responseCode(responseCodeSuccess).build();
        }
        throw new RuntimeException("User with identification number not found");
    }

    @Override
    public List<UserDto> readAllUsers() {

        List<User> users = userRepository.findAll();

        Map<String, UserRepresentation> userRepresentationMap = keycloakService.readUsers(users.stream().map(user -> user.getAuthId()).collect(Collectors.toList()))
                .stream().collect(Collectors.toMap(UserRepresentation::getId, Function.identity()));

        return users.stream().map(user -> {
            UserDto userDto = userMapper.convertToDto(user);
            UserRepresentation userRepresentation = userRepresentationMap.get(user.getAuthId());
            userDto.setUserId(user.getUserId());
            userDto.setEmailId(userRepresentation.getEmail());
            userDto.setIdentificationNumber(user.getIdentificationNumber());
            return userDto;
        }).collect(Collectors.toList());
    }

    @Override
    public UserDto readUser(String authId) {

        User user = userRepository.findUserByAuthId(authId).
                orElseThrow(() -> new ResourceNotFound("User not found on the server"));

        UserRepresentation userRepresentation = keycloakService.readUser(authId);
        UserDto userDto = userMapper.convertToDto(user);
        userDto.setEmailId(userRepresentation.getEmail());
        return userDto;
    }

    @Override
    public Response updateUserStatus(Long id, UserUpdateStatus userUpdate) {

        User user = userRepository.findById(id).orElseThrow(
                () -> new ResourceNotFound("User not found on the server"));

        if (FieldChecker.hasEmptyFields(user)) {
            log.error("User is not updated completely");
            throw new EmptyFields("please updated the user", responseCodeNotFound);
        }

        if(userUpdate.getStatus().equals(Status.APPROVED)){
            UserRepresentation userRepresentation = keycloakService.readUser(user.getAuthId());
            userRepresentation.setEnabled(true);
            userRepresentation.setEmailVerified(true);
            keycloakService.updateUser(userRepresentation);
        }

        user.setStatus(userUpdate.getStatus());
        userRepository.save(user);

        return Response.builder()
                .responseMessage("User updated successfully")
                .responseCode(responseCodeSuccess).build();
    }

    @Override
    public UserDto readUserById(Long userId) {

        return userRepository.findById(userId)
                .map(user -> userMapper.convertToDto(user))
                .orElseThrow(() -> new ResourceNotFound("User not found on the server"));
    }

    @Override
    public Response updateUser(Long id, UserUpdate userUpdate) {

        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFound("User not found on the server"));

        user.setContactNo(userUpdate.getContactNo());
        BeanUtils.copyProperties(userUpdate, user.getUserProfile());
        userRepository.save(user);

        return Response.builder()
                .responseCode(responseCodeSuccess)
                .responseMessage("user updated successfully").build();
    }
}

Now that all the services are done, we will focus on the Controller Layer to expose these methods as API Endpoints.

Controller Layer

package org.training.user.service.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.training.user.service.model.dto.CreateUser;
import org.training.user.service.model.dto.UserDto;
import org.training.user.service.model.dto.UserUpdate;
import org.training.user.service.model.dto.UserUpdateStatus;
import org.training.user.service.model.dto.response.Response;
import org.training.user.service.service.UserService;

import java.util.List;

@Slf4j
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/register")
    public ResponseEntity<Response> createUser(@RequestBody CreateUser userDto) {
        log.info("creating user with: {}", userDto.toString());
        return ResponseEntity.ok(userService.createUser(userDto));
    }

    @GetMapping
    public ResponseEntity<List<UserDto>> readAllUsers() {
        return ResponseEntity.ok(userService.readAllUsers());
    }

    @GetMapping("auth/{authId}")
    public ResponseEntity<UserDto> readUserByAuthId(@PathVariable String authId) {
        log.info("reading user by authId");
        return ResponseEntity.ok(userService.readUser(authId));
    }

    @PatchMapping("/{id}")
    public ResponseEntity<Response> updateUserStatus(@PathVariable Long id, @RequestBody UserUpdateStatus userUpdate) {
        log.info("updating the user with: {}", userUpdate.toString());
        return new ResponseEntity<>(userService.updateUserStatus(id, userUpdate), HttpStatus.OK);
    }

    @PutMapping("{id}")
    public ResponseEntity<Response> updateUser(@PathVariable Long id, @RequestBody UserUpdate userUpdate) {
        return new ResponseEntity<>(userService.updateUser(id, userUpdate), HttpStatus.OK);
    }

    @GetMapping("/{userId}")
    public ResponseEntity<UserDto> readUserById(@PathVariable Long userId) {
        log.info("reading user by ID");
        return ResponseEntity.ok(userService.readUserById(userId));
    }
}

All the configuration required in application.yml are given below:

server:
  port: 8082

spring:
  application:
    name: user-service
    bad_request: 400
    conflict: 409
    success: 200
    not_found: 404

  datasource:
    url: jdbc:mysql:localhost:3306/user_service
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true

app:
  config:
    keycloak:
      server-url: http://localhost:8571
      realm: banking-service
      client-id: banking-service-api-client
      client-secret: 932OFITe8vtH4z0G9gem4Afd69JhbwrI

Now, add the route defination(if not added) in the API Gateway that we created in Mastering Microservices: Setting API Gateway with Spring Cloud Gateway to allow the API Gateway to identify this service.

- id: user-service
  uri: lb://user-service
  predicates:
    - Path=/api/users/**

If any of you receive an exception when communication between the keycloak instance and service like:

java.lang.NoSuchMethodError: javax.ws.rs.core.UriBuilder.resolveTemplates(Ljava/util/Map;)Ljavax/ws/rs/core/UriBuilder;

at org.keycloak.admin.client.Keycloak.realm(Keycloak.java:114)

This issue primarily arises from the Eureka client dependency we are using. We need to exclude the javax.ws.rs.jsr311-api module, as it is the main cause of the exception we are encountering, so update the dependency as follows.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <exclusions>
        <exclusion>
             <groupId>javax.ws.rs</groupId>
             <artifactId>jsr311-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Note: Please don't try to run the application, since we need to add inter - service configuration and exception handling configuration.

Postman Collection

I'm using Postman to test the APIs, and I will be attaching the Postman collection below so that you can go through it.

Run In Postman

Conclusion

Thanks for reading our latest article on Mastering Microservices: Implemenatation of User Service with practical usage.

You can get source code for this tutorial from our GitHub repository.

Happy Coding!!!!😊

Did you find this article valuable?

Support Karthik Kulkarni by becoming a sponsor. Any amount is appreciated!

Β