Skip to content

e. Repository

Now, with the controller implemented, we can move on to the service layer of the Account microservice. The service layer is responsible for the business logic of the microservice, and it interacts with the repository layer to handle the data persistence of the Account entity. The service layer is implemented in the AccountService class, which contains the methods for creating, deleting, finding, and updating accounts. The AccountService class uses the AccountRepository to handle the data persistence of the Account entity, and the AccountParser to handle the parsing of the input and output of the API endpoints.

From previous hands-on, we have already see that this microservice uses clean architecture, which means that the service layer is independent of the controller layer and the repository layer. This allows us to easily test the business logic of the service layer without having to worry about the details of the controller layer or the repository layer. The following diagram illustrates the architecture of the Account microservice, showing the relationships between the different layers and classes:

sequenceDiagram
    title Clean architecture's approach 
    Actor Request
    participant Controller as Controller<br>Resource
    participant Service@{ "type": "control" }
    participant Repository@{ "type" : "entity" }
    participant Database@{ "type" : "database" }
    Request ->>+ Controller: Rest API (JSON)
    Controller ->>+ Service: parser (AccountIn -> Account)
    Service ->>+ Repository: parser (Account -> AccountModel)
    Repository ->>+ Database: 
    Database ->>- Repository: 
    Repository ->>- Service: parser (Account <- AccountModel)
    Service ->>- Controller: parser (AccountOut <- Account)
    Controller ->>- Request: Rest API (JSON)

The relationships between the different layers is done though the use of different DTOs (Data Transfer Objects). Then, to translate between the different layers, we use parsers, which are responsible for converting the DTOs to the entities and vice versa.

  • The AccountParser class is responsible for this translation, and it contains the methods for converting the AccountIn DTO to the Account entity, and the Account entity to the AccountOut DTO.

  • Also AccountModel is the class that represents the database model of the Account entity, which is used by the AccountRepository to handle the data persistence of the Account entity. The AccountModel class contains the attributes of the Account entity and it is annotated with JPA annotations to define the mapping between the class and the database table.

1. The Object-Relational Mapping (ORM)

This project uses a Relational Database (PostgreSQL) to store the data of the Account entity. But, the Java programming language is an object-oriented programming language, which means that we need a way to map the relational data to the object-oriented data

Object-Relational Mapping (ORM) is a programming technique that allows developers to interact with a relational database using an object-oriented programming language. It provides a way to map database tables to classes and database records to objects, allowing developers to work with data in a more intuitive and natural way. In the context of the Account microservice, we will use an ORM framework, JPA, to handle the data persistence for the Account entity, which will allow us to easily create, read, update, and delete accounts in the database.

Here, we can see the code that solves the data persistence concerns of the Account microservice, including the AccountModel class, which represents the database model of the Account entity, and the AccountRepository interface, which is responsible for the data persistence of the Account entity using JPA.

2. Database Migrations

Once we have the ORM set up, we need to manage our database schema changes. As our application evolves, we may need to add new tables, modify existing tables, or change the data types of columns. To manage these changes, we will use a database migration tool called Flyway1.

Flyway is a database migration tool that allows us to manage and version our database schema changes. It provides a way to define and execute database migrations, which are scripts that modify the database schema, such as creating tables, adding columns, or changing data types. In the context of the Account microservice, we will use Flyway to manage our database migrations, ensuring that our database schema is always up-to-date and consistent across different environments.

In the code, we have a db/migration directory, which contains the migration scripts for the Account microservice. Each migration script is named in a specific format, starting with a version number (e.g., V2026.02.27.001) followed by a description of the migration (e.g., create_schema). These migration scripts will be executed in order by Flyway to ensure that our database schema is always up-to-date.

3. Code

Let's go code the implementation of the Account microservice, which consists of a lot of classes. The resulting directory structure will look like this:

📁 api/
├── 📁 account/
├── 📁 account-service/
   ├── 📁 src/
      └── 📁 main/
          ├── 📁 java/
             └── 📁 store/
                 └── 📁 account/
                     ├──  Account.java
                     ├──  AccountApplication.java
                     ├──  AccountModel.java
                     ├──  AccountParser.java
                     ├──  AccountRepository.java
                     ├──  AccountResource.java
                     └──  AccountService.java
          └── 📁 resources/
              ├── 📁 db/
                 └── 📁 migration/
                     ├──  V2026.03.27.001__create_schema.sql
                     ├──  V2026.03.27.002__create_table.sql
                     └──  V2026.03.06.001__create_field_pass_sha256.sql
              └──  application.yaml
   └──  pom.xml
└──  compose.yaml

Where, respecting the clean architecture, we have the following classes:

Class Description
Account This class represents the Account entity, which is the main entity of the Account microservice. It contains the attributes of the Account entity, such as id, name, email, password, and sha256.
AccountModel This class represents the Account model, which is responsible for the persistence logic of the Account microservice. It contains the methods for creating, deleting, finding, and updating accounts.
AccountParser This class is responsible for parsing the input and output of the API endpoints, converting the AccountIn and AccountOut DTOs to the Account entity, and vice versa.
AccountRepository This interface is responsible for the data persistence of the Account entity, using an Object-Relational Mapping (ORM) framework to interact with the database.
AccountResource This class is responsible for the API endpoints of the Account microservice, implementing the AccountController interface defined in the account module, and using the AccountService to handle the business logic of the API endpoints.
AccountService This class is responsible for the business logic of the Account microservice, using the AccountRepository to handle the data persistence of the Account entity, and the AccountParser to handle the parsing of the input and output of the API endpoints.

Source

name: store-api

services:

  db:
    image: postgres:17
    hostname: db
    ports:
      - 5432:5432
    volumes:
      - ${VOLUME_DB}:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: ${DB_USER:-store}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-devpass}
      POSTGRES_DB: ${DB_NAME:-store}

  account:
    build:
      context: ./account-service
      dockerfile: Dockerfile
    hostname: account
    ports:
      - 8080:8080
    environment:
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_DB: ${DB_NAME:-store}
      DATABASE_USERNAME: ${DB_USER:-store}
      DATABASE_PASSWORD: ${DB_PASSWORD:-devpass}
    depends_on:
      - db
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>store</groupId>
    <artifactId>account-service</artifactId>
    <version>1.0.0</version>
    <name>account-service</name>

    <properties>
        <java.version>25</java.version>
        <spring-cloud.version>2025.1.0</spring-cloud.version>
        <maven.compiler.proc>full</maven.compiler.proc>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>store</groupId>
            <artifactId>account</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-flyway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-database-postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>

    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
server:
  port: 8080

spring:
  application:
    name: account

  datasource:
    url: jdbc:postgresql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_DB}
    username: ${DATABASE_USERNAME}
    password: ${DATABASE_PASSWORD}
    driver-class-name: org.postgresql.Driver

  flyway:
    baseline-on-migrate: true
    schemas: accounts

  jpa:
    properties:
      hibernate:
        default_schema: accounts
package store.account;

import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Builder @Accessors(chain = true, fluent = true)
public class Account {

    private String id;
    private String name;
    private String email;
    private String password;
    private String passwordSha256;

}
package store.account;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;

@Entity
@Table(name = "accounts")
@Setter @Accessors(chain = true, fluent = true)
@NoArgsConstructor @AllArgsConstructor
public class AccountModel {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @Column(name = "id")
    private String id;

    @Column(name = "name")
    private String name;

    @Column(name = "email")
    private String email;

    @Column(name = "password_sha256")
    private String passwordSha256;

    public AccountModel(Account a) {
        this.id = a.id();
        this.name = a.name();
        this.email = a.email();
        this.passwordSha256 = a.passwordSha256();
    }

    public Account to() {
        return Account.builder()
            .id(this.id)
            .name(this.name)
            .email(this.email)
            .passwordSha256(this.passwordSha256)
            .build();
    }

}
package store.account;

import java.util.List;

public class AccountParser {

    public static AccountOut to(Account a) {
        return a == null ? null :
            AccountOut.builder()
                .id(a.id())
                .name(a.name())
                .email(a.email())
                .build();
    }

    public static List<AccountOut> to(List<Account> l) {
        return l.stream().map(AccountParser::to).toList();
    }

    public static Account to(AccountIn in) {
        return in == null ? null :
            Account.builder()
                .name(in.name())
                .email(in.email())
                .password(in.password())
                .build();
    }

}
1
2
3
4
5
6
7
package store.account;

import org.springframework.data.repository.CrudRepository;

public interface AccountRepository extends CrudRepository<AccountModel, String>  {

}
package store.account;

import java.net.URI;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

@RestController
public class AccountResource implements AccountController {

    @Autowired
    private AccountService accountService;

    @Override
    public ResponseEntity<Void> create(AccountIn in) {
        final Account a = accountService.create(
            AccountParser.to(in)
        );
        // returns a JSON in the HATEAOS standard.
        return ResponseEntity.created(
            ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(a.id())
                .toUri()
        ).build();
    }

    @Override
    public ResponseEntity<Void> delete(String id) {
        accountService.delete(id);
        return ResponseEntity.noContent().build();
    }

    @Override
    public ResponseEntity<Void> healthCheck() {
        return ResponseEntity.ok().build();
    }

    @Override
    public ResponseEntity<List<AccountOut>> findAll() {
        return ResponseEntity.ok(
            AccountParser.to(
                accountService.findByAll()
            )
        );
    }

    @Override
    public ResponseEntity<AccountOut> findById(String id) {
        Account out = accountService.findById(id);
        return out == null ?
            ResponseEntity.notFound().build() :
            ResponseEntity.ok(
                AccountParser.to(out) // transform from account to ou
            );
    }

}
package store.account;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.List;
import java.util.stream.StreamSupport;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

    public Account create(Account account) {

        if (account.password() == null || account.password().trim().length() == 0) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Password is empty");
        }

        account.passwordSha256(calcHash(account.password()));

        return accountRepository.save(
            new AccountModel(account)
        ).to();
    }

    public void delete(String id) {
        accountRepository.deleteById(id);
    }

    public Account findById(String id) {
        return accountRepository.findById(id).orElse(null).to();
    }

    public List<Account> findByAll() {
        return StreamSupport.stream(
            accountRepository.findAll().spliterator(),
            false // transform from stream to list
        ).map(AccountModel::to) // parser from Model to Account
        .toList();
    }

    private String calcHash(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(text.getBytes(StandardCharsets.UTF_8));
            byte[] digest = md.digest();
            return Base64.getEncoder().encodeToString(digest);
        } catch (NoSuchAlgorithmException e) {
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
        }
    }

}
CREATE SCHEMA IF NOT EXISTS accounts;
1
2
3
4
5
CREATE TABLE accounts.accounts (
    id VARCHAR(36) PRIMARY KEY,
    name VARCHAR(256) NOT NULL,
    email VARCHAR(256) NOT NULL UNIQUE
);
ALTER TABLE accounts.accounts
ADD COLUMN password_sha256 VARCHAR(64) NOT NULL;

4. Running

To run the Account microservice, we can use the docker-compose command to start the microservice and its dependencies (e.g., the PostgreSQL database). Make sure you are in the root directory of the project, where the compose.yaml file is located, and run the following command:

docker compose up --build

Note that the --build flag is used to build the Docker images before starting the containers, which is necessary if you have made changes to the code or the Dockerfile.

Also, we can monitoring the logs of the microservice to see if it is running correctly. See there that the Flyway migrations are being executed, and that the microservice is starting up without any errors. You should see logs indicating that the microservice is up and running, and that it is connected to the database. In additional, you can also check the database to see if the migrations have been executed correctly, and that the tables have been created as expected.

  1. Going inside the container:

    Entering the container
    docker exec -it account-service bash
    
  2. Going inside the PostgreSQL database:

    Entering the PostgreSQL database
    psql -U store -d store
    
  3. Verifying the migrations:

    List the schemas
    \dn
    
    List the tables
    \dt *.*
    

Yet, if you want to monitor the logs of the microservice without going inside the container, you can use the docker logs command to view the logs of the container. This will allow you to see the logs in real-time, and you can use it to monitor the startup process of the microservice, as well as any errors or issues that may arise.

Monitoring the logs
docker logs -f account-service

Done!