Hello folks, now we have completed basic implementation for core banking service along with API gateway and service registry setup. Now let’s focus on developing user service to support user web service tasks inside this internet banking application.

So what are the basic functionalities here we are going to develop?

  • Register user with keycloak REST API and keep authID on local DB.
  • Update user.
  • Read registered users and bind additional data from keycloak.
  • Read all registered users.

Developing User Service

Just create another spring boot application using Spring Initializr with the following dependencies,

  • Spring Web
  • Lombok
  • Spring Data JPA
  • Flyway
  • MySQL
  • Eureka Discovery Client
Create spring boot application with Spring Initializr
Create spring boot application with Spring Initializr

Database Models

In this user service we are going to store basic user related data, But mainly we are going to use Keycloak for base user management part, and Here we are going to store only the Auth ID in local db to match with keycloak user when needed.

Just create UserEntity.java in model.entity package, and There is a Status enum that we will go to use with this entity class.

package com.javatodev.finance.model.dto;

public enum Status {
    PENDING, APPROVED, DISABLED, BLACKLIST
}
package com.javatodev.finance.model.entity;

import com.javatodev.finance.model.dto.Status;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Getter
@Setter
@Entity
@Table(name = "user")
public class UserEntity {

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

    private String authId;

    private String identification;

    @Enumerated(EnumType.STRING)
    private Status status;

}

Repository Layer

Just create UserRepository.java in model.repository package,

package com.javatodev.finance.model.repository;

import com.javatodev.finance.model.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<UserEntity, Long> {
}

DTO, Mapper, and REST Request and Responses

Here I’m going to use separate DTO layer to bring data from this core service to public over the API. Basically here I’ll not going to expose my entity layer, instead of that I’ll define DTO layer to bring in and out data from this API.

package com.javatodev.finance.model.dto;

import lombok.Data;

@Data
public class User {
    private Long id;

    private String email;

    private String identification;

    private String password;

    private String authId;

    private Status status;
}
package com.javatodev.finance.model.dto;

import lombok.Data;

@Data
public class UserUpdateRequest {
    private Status status;
}

Now we need to have a common implementation to map things from Entity->DTO and vice versa, Let’s call it BaseMapper.java and extend that when we need on separate aspects.

package com.javatodev.finance.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> entity, Object... args) {
        return entity.stream().map(e -> convertToDto(e, args)).collect(Collectors.toList());
    }

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

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

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

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


}

Then create User mapper which maps UserEntity to User DTO and Vise versa.

package com.javatodev.finance.model.mapper;

import com.javatodev.finance.model.dto.User;
import com.javatodev.finance.model.entity.UserEntity;
import org.springframework.beans.BeanUtils;

public class UserMapper extends BaseMapper<UserEntity, User>{
    @Override
    public UserEntity convertToEntity(User dto, Object... args) {
        UserEntity userEntity = new UserEntity();
        if (dto != null) {
            BeanUtils.copyProperties(dto, userEntity);
        }
        return userEntity;
    }

    @Override
    public User convertToDto(UserEntity entity, Object... args) {
        User user = new User();
        if (entity != null) {
            BeanUtils.copyProperties(entity, user);
        }
        return user;
    }
}

Done, now we have all the components which need to have inside this internet banking user service in order to access DB and read/write data from that. Let’s add service implementation and controller implementation to complete this API.

Service Implementation

Here I’m going to write 2 Service implementations to communicate with the keycloak user keycloak admin library and another one to communicate DB via repositories.

Keycloak Service

Let’s start with Keycloak service which enables communication between our user service and keycloak instance.

First, you need to add a client in keycloak instance which has the capability of authenticating with the client id and a secret. Just logged in to the admin panel and create a new client with naming as ‘internet-banking-api-client’. Most importantly turn on the Service account enable for that client which enables us to authenticate with client id and sercret.

create client with service account enable in keycloak
create client with service account enable in keycloak
setting credentials to created client in keycloak
setting credentials to created client in keycloak

Ok now we have working client which we could use with our API service. There is a another library which we should add to the user service in order to enable communication with keycloak.

add following keycloak admin client dependency into the gradle file in user service,

implementation 'org.keycloak:keycloak-admin-client:12.0.4'

After that set following keycloak properties in the application.yml,

app:
  config:
    keycloak:
      server-url: http://localhost:8080/auth
      realm: javatodev-internet-banking
      clientId: internet-banking-api-client
      client-secret: e8548d56-d743-45ef-8655-063c9cd96759

Then create a property KeycloakProperties.java class to easily map these property values into the spring boot service application.

package com.javatodev.finance.configuration;

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.clientId}")
    private String clientId;

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

    private static Keycloak keycloakInstance = null;

    public Keycloak getInstance() {

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

    public String getRealm() {
        return realm;
    }
}

After that create a class named KeyCloakManager.java and add the following content into that, With this, we can easily get a RealmResouce which has the capability of communicating with keycloak instance,

package com.javatodev.finance.configuration;

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.getInstance().realm(keycloakProperties.getRealm());
    }

}

Finally, the service implementation to keycloak reads and writes, Here in this service implementation we are creating users in keycloak, update keycloak user data, read user by email and read user by auth id.

package com.javatodev.finance.service;

import com.javatodev.finance.configuration.KeycloakManager;
import lombok.RequiredArgsConstructor;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.stereotype.Service;

import javax.ws.rs.core.Response;
import java.util.List;

@Service
@RequiredArgsConstructor
public class KeycloakUserService {

    private final KeycloakManager keyCloakManager;

    public Integer createUser(UserRepresentation userRepresentation) {
        Response response = keyCloakManager.getKeyCloakInstanceWithRealm().users().create(userRepresentation);
        return response.getStatus();
    }

    public void updateUser(UserRepresentation userRepresentation) {
        keyCloakManager.getKeyCloakInstanceWithRealm().users().get(userRepresentation.getId()).update(userRepresentation);
    }


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


    public UserRepresentation readUser(String authId) {
        try {
            UserResource userResource = keyCloakManager.getKeyCloakInstanceWithRealm().users().get(authId);
            return userResource.toRepresentation();
        } catch (Exception e) {
            throw new RuntimeException("User not found under given ID");
        }
    }
}

User Service

In this service implementation I’m going to develop few methods which has capability of create user, read users with pagination, read user by auth id, read user by id and finally update user.

package com.javatodev.finance.service;

import com.javatodev.finance.model.dto.Status;
import com.javatodev.finance.model.dto.User;
import com.javatodev.finance.model.dto.UserUpdateRequest;
import com.javatodev.finance.model.entity.UserEntity;
import com.javatodev.finance.model.mapper.UserMapper;
import com.javatodev.finance.model.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import javax.persistence.EntityNotFoundException;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
    private final KeycloakUserService keycloakUserService;
    private final UserRepository userRepository;

    private UserMapper userMapper = new UserMapper();

    public User createUser(User user) {

        List<UserRepresentation> userRepresentations = keycloakUserService.readUserByEmail(user.getEmail());
        if (userRepresentations.size() > 0) {
            throw new RuntimeException("This email already registered as a user. Please check and retry.");
        }

        UserRepresentation userRepresentation = new UserRepresentation();
        userRepresentation.setEmail(user.getEmail());
        userRepresentation.setEmailVerified(false);
        userRepresentation.setEnabled(false);
        userRepresentation.setUsername(user.getEmail());

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

        Integer userCreationResponse = keycloakUserService.createUser(userRepresentation);

        if (userCreationResponse == 201) {
            log.info("User created under given username {}", user.getEmail());

            List<UserRepresentation> userRepresentations1 = keycloakUserService.readUserByEmail(user.getEmail());
            user.setAuthId(userRepresentations1.get(0).getId());
            user.setStatus(Status.PENDING);
            user.setIdentification(UUID.randomUUID().toString());
            UserEntity save = userRepository.save(userMapper.convertToEntity(user));
            return userMapper.convertToDto(save);
        }

        throw new RuntimeException("We couldn't find user under given identification. Please check and retry");

    }

    public List<User> readUsers(Pageable pageable) {
        Page<UserEntity> allUsersInDb = userRepository.findAll(pageable);
        List<User> users = userMapper.convertToDtoList(allUsersInDb.getContent());
        users.forEach(user -> {
            UserRepresentation userRepresentation = keycloakUserService.readUser(user.getAuthId());
            user.setId(user.getId());
            user.setEmail(userRepresentation.getEmail());
            user.setIdentification(user.getIdentification());
        });
        return users;
    }

    public User readUser(Long userId) {
        return userMapper.convertToDto(userRepository.findById(userId).orElseThrow(EntityNotFoundException::new));
    }

    public User updateUser(Long id, UserUpdateRequest userUpdateRequest) {
        UserEntity userEntity = userRepository.findById(id).orElseThrow(EntityNotFoundException::new);

        if (userUpdateRequest.getStatus() == Status.APPROVED) {
            UserRepresentation userRepresentation = keycloakUserService.readUser(userEntity.getAuthId());
            userRepresentation.setEnabled(true);
            userRepresentation.setEmailVerified(true);
            keycloakUserService.updateUser(userRepresentation);
        }

        userEntity.setStatus(userUpdateRequest.getStatus());
        return userMapper.convertToDto(userRepository.save(userEntity));
    }

}

All done now we have completed full service implementations for this user service. Let’s focus on creating controller layer to expose these functions as API.

Controller Layer to Expose API Endpoints

package com.javatodev.finance.controller;

import com.fasterxml.jackson.annotation.JsonView;
import com.javatodev.finance.model.dto.User;
import com.javatodev.finance.model.dto.UserUpdateRequest;
import com.javatodev.finance.service.KeycloakUserService;
import com.javatodev.finance.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping(value = "/api/v1/bank-user")
@RequiredArgsConstructor
public class UserController {

    private final KeycloakUserService keycloakUserService;
    private final UserService userService;

    @PostMapping(value = "/register")
    public ResponseEntity createUser(@RequestBody User request) {
        log.info("Creating user with {}", request.toString());
        return ResponseEntity.ok(userService.createUser(request));
    }

    @PatchMapping(value = "/update/{id}")
    public ResponseEntity updateUser(@PathVariable("id") Long userId, @RequestBody UserUpdateRequest userUpdateRequest) {
        log.info("Updating user with {}", userUpdateRequest.toString());
        return ResponseEntity.ok(userService.updateUser(userId, userUpdateRequest));
    }

    @GetMapping
    public ResponseEntity readUsers(Pageable pageable) {
        log.info("Reading all users from API");
        return ResponseEntity.ok(userService.readUsers(pageable));
    }

    @GetMapping(value = "/{id}")
    public ResponseEntity readUser(@PathVariable("id") Long id) {
        log.info("Reading user by id {}", id);
        return ResponseEntity.ok(userService.readUser(id));
    }

}

Done, Add or update following properties in application.yml,

spring:
  application:
    name: internet-banking-user-service
  datasource:
    url: jdbc:mysql://localhost:3306/banking_core_user_service
    username: root
    password: password
  jpa:
    hibernate:
      ddl-auto: update

server:
  port: 8083

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8081/eureka

info:
  app:
    name: ${spring.application.name}

app:
  config:
    keycloak:
      server-url: http://localhost:8080/auth
      realm: javatodev-internet-banking
      clientId: internet-banking-api-client
      client-secret: e8548d56-d743-45ef-8655-063c9cd96759

Then there is one more thing to do in the API gateway we developed earlier in Microservices – Setup API Gateway Using Spring Cloud Gateway, Just add following route definition to the API gateway in order to access this user service.

## USER SERVICE
- id: internet-banking-user-service
  uri: lb://banking-core-user-service
  predicates:
    - Path=/user/**
  filters:
    - StripPrefix=1

All done, But there is one more thing, If someone experiencing issue while communicating with keycloak over user service like below,

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)

Just update eureka client dependency import in your gradle file with excluding javax.ws.rs.jsr311-api, which is not compatible with this setup.

implementation ('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client') {
    exclude group: 'javax.ws.rs', module: 'jsr311-api'
}

Testing API with Postman

Just start all the components including service registry, API gateway, and internet banking user service, and test API over a gateway to check the implementation.

Internet Banking User Service Up and Running with API gateway - user service implementation
Internet Banking User Service Up and Running with API gateway

Here are a few screenshots while test the user service API using Postman, and you can access the same collection using the below link.

Run in Postman
Create a new user - user service implementation
Create a new user
Read all users from user core service API - user service implementation
Read all users from user core service API

Conclusion

Thanks for reading our latest article on Microservices – User Service Implementation with practical usage.

Now we have fully implemented the basic internet banking user service solution for this series and let’s focus on completing internet banking fund transfer service in our next article.

If you are looking for spring boot practical application development tutorials, just check our article series.

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