Skip to content

1. CRUD

Account Microservice

The Account microservice is responsible for managing user accounts, basically, almost every application has a user account system. This microservice provides the necessary endpoints to create, read, update, and delete accounts. The microservice is built using Spring Boot and follows the Domain-Driven Design (DDD) approach.

The microservice is divided into two main modules: account and account-service:

  • the account module contains the API definition and the data transfer objects (DTOs) for the Account microservice;
  • the account-service module contains the service implementation, repository, and entity classes.
classDiagram
    namespace account {
        class AccountController {
            +create(AccountIn accountIn): AccountOut
            +delete(String id): void
            +findAll(): List<AccountOut>
            +findById(String id): AccountOut
        }
        class AccountIn {
            -String name
            -String email
            -String password
        }
        class AccountOut {
            -String id
            -String name
            -String email
        }
    }
    namespace account-service {
        class AccountResource {
            +create(AccountIn accountIn): AccountOut
            +delete(String id): void
            +findAll(): List<AccountOut>
            +findById(String id): AccountOut
        }
        class AccountService {
            +create(AccountIn accountIn): AccountOut
            +delete(String id): void
            +findAll(): List<AccountOut>
            +findById(String id): AccountOut
        }
        class AccountRepository {
            +create(AccountIn accountIn): AccountOut
            +delete(String id): void
            +findAll(): List<AccountOut>
            +findById(String id): AccountOut
        }
        class Account {
            -String id
            -String name
            -String email
            -String password
            -String sha256
        }
        class AccountModel {
            +create(AccountIn accountIn): AccountOut
            +delete(String id): void
            +findAll(): List<AccountOut>
            +findById(String id): AccountOut
        }
    }
    <<Interface>> AccountController
    AccountController ..> AccountIn
    AccountController ..> AccountOut

    <<Interface>> AccountRepository
    AccountController <|-- AccountResource
    AccountResource *-- AccountService
    AccountService *-- AccountRepository
    AccountService ..> Account
    AccountService ..> AccountModel
    AccountRepository ..> AccountModel

This approach allows the separation of concerns and the organization of the codebase into different modules, making it easier to maintain and scale the application. Also, it creates a facility to reuse the microservice by other microservices in the future - builts in Java.

The construction of the Account microservice follows the Clean Architecture approach, which promotes the total decoupling of business rules from interface layers. The diagram below illustrates the flow of data among the layers of the Account microservice:

sequenceDiagram
    title Clean architecture's approach 
    Actor Request
    Request ->>+ Controller: 
    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: 

Previously to build the Account microservice, it is necessary to prepare the environment by installing the database to persist the data. For that, we will use a Docker Compose file to create a PostgreSQL container, as well as, a cluster to isolate the microservices from external access, creating a secure environment - trusted layer. A Docker Compose file is a YAML file that defines how Docker containers should behave in production. The file contains the configuration for the database, the microservices, and the network configuration.

flowchart LR
    subgraph api [Trusted Layer]
        direction TB
        account e3@==> db@{ shape: cyl, label: "Database" }
    end
    internet e1@==>|request| account:::red
    e1@{ animate: true }
    e3@{ animate: true }
    classDef red fill:#fcc

Docker Compose

📁 api/
├── 📁 account/
├── 📁 account-service/
├── 📄 .env
└── 📄 compose.yaml
name: store-api

services:

  db:
    image: postgres:17.6
    hostname: db
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    # ports:
    #   - 5432:5432
    volumes:
      - ./volume/db:/var/lib/postgresql/data

  account:
    build:
      context: ./pma252.account-service
      dockerfile: Dockerfile
    hostname: account
    environment:
      DATABASE_URL: jdbc:postgresql://db:5432/${POSTGRES_DB}
      DATABASE_USERNAME: ${POSTGRES_USER}
      DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
    restart: always
    deploy:
      replicas: 1
    depends_on:
      - db

  auth:
    build:
      context: ./pma252.auth-service
      dockerfile: Dockerfile
    hostname: auth
    environment:
      JWT_SECRET_KEY: ${JWT_SECRET_KEY}


  gateway:
    build:
      context: ./pma252.gateway-service
      dockerfile: Dockerfile
    ports:
      - 8080:8080
1
2
3
4
POSTGRES_USER=store
POSTGRES_PASSWORD=I5pEr@5ecReT
POSTGRES_DB=store
JWT_SECRET_KEY=7e4e002c4da04ce8c0bdcf9fdf332505334ebba7cac63a37b630c432879a4a63a6ebf26e8f2c19d9ce64f8ffe46d223749dbaa1184b02b50d083369b7fc798b8
docker compose up -d --build
[+] Running 2/2
✔ Network store_default Created 0.1s
✔ Container store-db-1 Started 0.2s

Account

📁 api/
└── 📁 account/
    ├── 📁 src/
       └── 📁 main/
           └── 📁 java/
               └── 📁 store/
                   └── 📁 account/
                       ├── 📄 AccountController.java
                       ├── 📄 AccountIn.java
                       └── 📄 AccountOut.java
    └── 📄 pom.xml
Source
<?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>3.5.5</version>
        <relativePath/>
    </parent>

    <groupId>store</groupId>
    <artifactId>account</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>21</java.version>
        <spring-cloud.version>2025.0.0</spring-cloud.version>
        <maven.compiler.proc>full</maven.compiler.proc>
    </properties>

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

</project>
AccountController.java
package store.account;

import java.util.List;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(name = "account", url = "http://account:8080")
public interface AccountController {

    @PostMapping("/account")
    public ResponseEntity<AccountOut> create(
        @RequestBody AccountIn in
    );

    @GetMapping("/account/{id}")
    public ResponseEntity<AccountOut> findById(
        @PathVariable("id") String id
    );

    @PostMapping("/account/login")
    public ResponseEntity<AccountOut> findByEmailAndPassword(
        @RequestBody AccountIn in
    );

    @GetMapping("/account")
    public ResponseEntity<List<AccountOut>> findAll();

    @DeleteMapping("/account/{id}")
    public ResponseEntity<Void> delete(
        @PathVariable("id") String id
    );

}
AccountIn.java
package store.account;

import lombok.Builder;

@Builder
public record AccountIn(
    String name,
    String email,
    String password
) {

}
AccountOut.java
package store.account;

import lombok.Builder;

@Builder
public record AccountOut(
    String id,
    String name,
    String email
) {

}

mvn clean install

Account-Service

📁 api/
└── 📁 account-service/
    ├── 📁 src/
       └── 📁 main/
           ├── 📁 java/
              └── 📁 store/
                  └── 📁 account/
                      ├── 📄 Account.java
                      ├── 📄 AccountApplication.java
                      ├── 📄 AccountModel.java
                      ├── 📄 AccountParser.java
                      ├── 📄 AccountRepository.java
                      ├── 📄 AccountResource.java
                      └── 📄 AccountService.java
           └── 📁 resources/
               ├── 📄 application.yaml
               └── 📁 db/
                   └── 📁 migration/
                       ├── 📄 V2025.08.29.001__create_schema.sql
                       ├── 📄 V2025.08.29.002__create_table_account.sql
                       └── 📄 V2025.09.02.001__create_index_email.sql
    ├── 📄 pom.xml
    └── 📄 Dockerfile
Source
<?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>3.5.5</version>
        <relativePath/>
    </parent>

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

    <properties>
        <java.version>21</java.version>
        <spring-cloud.version>2025.0.0</spring-cloud.version>
        <maven.compiler.proc>full</maven.compiler.proc>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

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

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-database-postgresql</artifactId>
        </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>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
1
2
3
4
FROM openjdk:25-slim
VOLUME /tmp
COPY target/*.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
server:
  port: 8080

spring:
  application:
    name: account

  mvc:
    problemdetails:
      enabled: true

  datasource:
    url: ${DATABASE_URL}
    username: ${DATABASE_USERNAME}
    password: ${DATABASE_PASSWORD}
    driver-class-name: org.postgresql.Driver

  flyway:
    baseline-on-migrate: true
    schemas: account
  jpa:
    properties:
      hibernate:
        default_schema: account

logging:
  level:
    store: debug
package store.account;

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

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

    private String id;
    private String name;
    private String email;
    private String password;
    private String sha256;

}
package store.account;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients(basePackages = {
})
@SpringBootApplication
public class AccountApplication {

    public static void main(String[] args) {
        SpringApplication.run(AccountApplication.class, args);
    }

}
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 = "account")
@Setter @Accessors(chain = true, fluent = true)
@NoArgsConstructor @AllArgsConstructor
public class AccountModel {

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

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

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

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

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

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

}
package store.account;

import java.util.List;

public 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> as) {
        return as == null ? null :
            as.stream().map(AccountParser::to).toList();
    }

}
package store.account;

import java.util.Optional;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AccountRepository extends CrudRepository<AccountModel, String> {

    AccountModel findByEmail(String email);
    Optional<AccountModel> findByEmailAndSha256(String email, String sha256);

}
package store.account;

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<AccountOut> create(AccountIn in) {
        // parser AccountIn to Account
        Account account = AccountParser.to(in);

        Account saved = accountService.create(account);

        // parser Account to AccountOut and build to
        // HATEAOS standard
        return ResponseEntity
            .created(
                ServletUriComponentsBuilder.fromCurrentRequest()
                    .path("/{id}")
                    .buildAndExpand(saved.id())
                    .toUri()
            ).body(AccountParser.to(saved));
    }

    @Override
    public ResponseEntity<AccountOut> findById(String id) {
        return ResponseEntity
            .ok(AccountParser.to(accountService.findById(id)));
    }

    @Override
    public ResponseEntity<AccountOut> findByEmailAndPassword(AccountIn in) {
        return ResponseEntity
            .ok()
            .body(AccountParser.to(
                accountService.findByEmailAndPassword(in.email(), in.password())
            ));
    }

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

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

}
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 {

    private Logger looger = LoggerFactory.getLogger(AccountService.class);

    @Autowired
    private AccountRepository accountRepository;

    public Account create(Account account) {
        if (null == account.password()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "Password is mandatory!"
            );
        }
        // clean special caracters
        account.password(account.password().trim());
        if (account.password().length() < 4) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "Password is too short!"
            );
        }
        if (null == account.email()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "Email is mandatory!"
            );
        }

        if (accountRepository.findByEmail(account.email()) != null)
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "Email already have been registered!"
            );

        account.sha256(hash(account.password()));

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

    public List<Account> findAll() {
        return StreamSupport.stream(
            accountRepository.findAll().spliterator(), false)
            .map(AccountModel::to)
            .toList();
    }

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

    public Account findByEmailAndPassword(String email, String password) {
        String sha256 = hash(password);
        return accountRepository.findByEmailAndSha256(email, sha256).map(AccountModel::to).orElse(null);
    }

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

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

}
CREATE SCHEMA IF NOT EXISTS account;
1
2
3
4
5
6
7
8
CREATE TABLE account
(
    id varchar(36) NOT NULL,
    name varchar(255) NOT NULL,
    email varchar(255) NOT NULL,
    sha256 varchar(64) NOT NULL,
    CONSTRAINT account_pkey PRIMARY KEY (id)
)
CREATE INDEX account_email_idx ON account (email);
mvn clean package spring-boot:run