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, finding (all, or one by id), and deleting 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.08.24.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
│   │       │           ├──  AccountServiceApplication.java
│   │       │           ├──  AccountModel.java
│   │       │           ├──  AccountParser.java
│   │       │           ├──  AccountRepository.java
│   │       │           ├──  AccountResource.java
│   │       │           └──  AccountService.java
│   │       └── 📁 resources/
│   │           ├── 📁 db/
│   │           │   └── 📁 migration/
│   │           │       ├──  V2026.08.24.001__create_schema.sql
│   │           │       └──  V2026.08.24.002__create_table.sql
│   │           └── YAML application.yaml
│   └──  pom.xml
└── YAML 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 (the raw value, only on the way in), and hashPassword.
AccountModel This class is the JPA entity mapped to the table accounts.accounts (id, name, email, hash_password). Its constructor AccountModel(Account) and its method to() convert between the entity and the model.
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. It extends CrudRepository and redeclares findAll() to return a List instead of an Iterable.
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

services:

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

  account:
    build:
      context: ./account-service
      dockerfile: Dockerfile
    ports:
      - 8080:8080
    environment:
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_DB: ${DB_NAME:-store}
      DATABASE_USERNAME: ${DB_USER:-store}
      DATABASE_PASSWORD: ${DB_PASSWORD:-store}
    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.1.1</version>
    </parent>
    <groupId>store</groupId>
    <artifactId>account-service</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>25</java.version>
        <spring-cloud.version>2025.1.3</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.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </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>

        <dependency>
            <groupId>store</groupId>
            <artifactId>account</artifactId>
            <version>1.0.0</version>
        </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.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                    <execution>
                        <id>default-testCompile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>
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;

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

    private String id;
    private String name;
    private String email;
    private String password;
    private String hashPassword;

}
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 = "hash_password")
    private String hashPassword;

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

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

}
package store.account;

import java.util.List;

public final class AccountParser {

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

    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> la) {
        return la.stream()
            .map(AccountParser::to)
            .toList();
    }

}
package store.account;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface AccountRepository extends CrudRepository<AccountModel, String> {

    List<AccountModel> findAll();

}
package store.account;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

@Controller
public class AccountResource implements AccountController {

    @Autowired
    private AccountService accountService;

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

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

    @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<AccountOut> findById(String id) {
        final Account a = accountService.findById(id);
        if (a == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        return ResponseEntity.ok(AccountParser.to(a));
    }

}
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 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 (null == account.password()) {
            throw new ResponseStatusException(
                HttpStatus.BAD_REQUEST, "Password is empty"
            );            
        }

        final String pass = account.password().trim();
        if (pass.length() < 6) {
            throw new ResponseStatusException(
                HttpStatus.BAD_REQUEST, "Password must have at least 6 characters"
            );
        }

        if (account.email() == null || account.email().trim().isEmpty()) {
            throw new ResponseStatusException(
                HttpStatus.BAD_REQUEST, "Email is empty"
            );
        }

        // agora calcula o hash
        account.hashPassword(calcHash(pass));

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

    public Account findById(String id) {
        // null when the id does not exist: AccountResource answers 404
        return accountRepository.findById(id)
            .map(AccountModel::to)
            .orElse(null);
    }

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

    public List<Account> findAll() {
        return accountRepository.findAll().stream()
            .map(AccountModel::to)
            .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
6
CREATE TABLE accounts.accounts (
    id VARCHAR(36) PRIMARY KEY,
    name VARCHAR(256) NOT NULL,
    email VARCHAR(256) NOT NULL UNIQUE,
    hash_password VARCHAR(256) NOT NULL
);

The business rules live in AccountService:

  • create answers 400 Bad Request when the password is missing or shorter than 6 characters, or when the email is empty; otherwise it stores only the SHA-256 hash (Base64) of the password, in hash_password — never the password itself.
  • findById returns null for an unknown id, and AccountResource turns it into 404 Not Found.

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 PostgreSQL database. psql lives in the db container (the postgres image), not in the account one, so open it there with the service name from compose.yaml and the user/database defined by DB_USER and DB_NAME (both store):

    Entering the PostgreSQL database
    docker compose exec db psql -U store -d store
    
  2. 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 compose logs command with the service name (account) 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 compose logs -f account

Done!