Skip to content

Škálovatelná Java Spring Boot Mikro-Servisní aplikace za pomocí Kubernetees a Dockeru. S frontendem v Next.js a Reactu

Notifications You must be signed in to change notification settings

ztomas-codes/SpringBoot-MicroServices

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 

Repository files navigation

MicroService Architektura ve Spring Bootu

Diagram

Diagram

Projekt, na kterém jsem se učil mikro servisní architekturu a zkoušel více psát škálovatelný software a pár dalších Patternů a vychytávek

  • K8S (Kubernetees)
  • Docker
  • Spring Boot
  • Java

Credit Service - CQRS Pattern ( Command Query Separation )

Jelikož dělám aplikaci, kterou chci hodně škálovat, tak v něčem jako je Credit Systém nesmí chybět CQRS. Funguje to tak, že je oddělený styl jakým se data zapisují a jakým se data čtou. Protože klasický CRUD není uplně optimální na zápis

├── Configs
│   └── KafkaProducerConfig.java
├── Controllers
│   └── CreditAccountController.java
├── CreditServiceApplication.java
├── Entities
│   ├── Events
│   │   ├── AccountCreationEvent.java
│   │   ├── CreditAdditionEvent.java
│   │   ├── CreditEvent.java
│   │   └── CreditRemovalEvent.java
│   ├── EventStore.java
│   ├── EventTypes.java
│   └── ReadModels
│       └── CreditAccount.java
├── Models
├── Repositories
│   ├── CreditAccountRepository.java
│   └── EventStoreRepository.java
├── Services
│   ├── CreditAccountEventConsumer.java
│   ├── CreditAccountEventProducer.java
│   └── CreditAccountService.java
└── Utils

Co jsem se naučil

  • CQRS
    • Command Query Separation ( Rozdělení na zápis a čtení - optimalizace)

Zápis dat

Zapisují se události (eventy), přes tzv. Producer:

public class CreditAccountEventProducer {
    private static final String TOPIC = "credit-accounts";
    private final KafkaTemplate<String, EventStore> kafkaTemplate;
    private final EventStoreRepository eventStoreRepository;

    public void createAndPushCreditEvent(CreditEvent event, Integer employeeId){
        ObjectMapper mapper = new ObjectMapper();
        String payload;
        try {
            payload = mapper.writeValueAsString(event);
        } catch (Exception e)
        {
            throw new RuntimeException("Deserialization failed: " + e.getMessage());
        }

        EventTypes type;
        switch(event.getClass().getSimpleName())
        {
            case "AccountCreationEvent":
                type = EventTypes.AccountCreation;
                break;
            case "CreditAdditionEvent":
                type = EventTypes.CreditAddition;
                break;
            case "CreditRemovalEvent":
                type = EventTypes.CreditRemoval;
                break;
            default:
                throw new RuntimeException("Incorrect type");
        }

        EventStore e = eventStoreRepository.save(EventStore
                .builder()
                .employeeId(employeeId)
                .eventType(type)
                .timestamp(LocalDateTime.now())
                .payload(payload)
                .build()
        );

        kafkaTemplate.send(TOPIC, e);
    }
}
     

do EventStore tabulky, a dále pošle event na Kafku. Mám potom listener který poslouchá od kafky eventy a dále s němi manipuluje..

public class CreditAccountEventConsumer {
    private final CreditAccountRepository creditAccountRepository;

    @KafkaListener(topics = "credit-accounts", groupId = "credit-account-group")
    public void handleEvent(EventStore event) {
        switch(event.getEventType())
        {
            case AccountCreation:
                handleAccountCreation(event);
                break;
            case CreditAddition:
                handleCreditAddition(event);
                break;
            case CreditRemoval:
                handleCreditRemoval(event);
                break;
        }
    }

    private void handleAccountCreation(EventStore event)
    {
        CreditAccount acc = CreditAccount
            .builder()
            .employeeId(event.getEmployeeId())
            .balance(0)
            .build();
        creditAccountRepository.save(acc);
    }

    private void handleCreditAddition(EventStore event)
    {
        ObjectMapper mapper = new ObjectMapper();
        CreditAdditionEvent e;
        try { 
            e = mapper.readValue(event.getPayload(), CreditAdditionEvent.class );
        } catch (Exception exception)
        {
            //TODO: DEAD QUEUE
            throw new RuntimeException("Error while reading payload");
        }

        CreditAccount acc = creditAccountRepository.
            findByEmployeeId(event.getEmployeeId())
            .orElseThrow(()-> new RuntimeException("User not found")); //TODO: DEAD QUEUE
        acc.setBalance(acc.getBalance() + e.getAmount());
        creditAccountRepository.save(acc); 
    }

    private void handleCreditRemoval(EventStore event)
    {
        ObjectMapper mapper = new ObjectMapper();
        CreditRemovalEvent e;
        try { 
            e = mapper.readValue(event.getPayload(), CreditRemovalEvent.class );
        } catch (Exception exception)
        {
            //TODO: DEAD QUEUE
            throw new RuntimeException("Error while reading payload");
        }

        CreditAccount acc = creditAccountRepository.
            findByEmployeeId(event.getEmployeeId())
            .orElseThrow(()-> new RuntimeException("User not found")); //TODO: DEAD QUEUE
        acc.setBalance(acc.getBalance() - e.getAmount());
        creditAccountRepository.save(acc); 
    }
}

Zde můžete vidět že odchytává eventy a zakládá potom ReadModely na čtení v tabulce credit_account (Nachovaný aktualní kreditový stav = Rychlé čtení). A zápis je řešený takto, protože pro zápis je mnohem rychlejší přidávat, než dělat update nebo delete, jelikož by musel locknout řádek přepsat atd..

  • Event Sorcing
    • Eventy / události jsou zdrojem dat
  • Kafka
    • Používám jako queue na eventy z event sourcingu, které potom poslouchám a když přijde na řadu tak ho odbavím tím, že ho zacachuji do ReadModelu credit_account tabulky
  • LoadBalancer ( Round Robin)
  • Queues ( Dead Queues)
    • Když nějaký event z nějakého důvodu selže, tak ho přidám do Dead Que pro další zpracování

Employee Service - Normalní CRUD

├── Configs
│   └── ModelMapperConfig.java
├── Controllers
│   ├── EmployeeController.java
│   └── EmployeeRestControllerAdvice.java
├── EmployeeServiceApplication.java
├── Entities
│   ├── Employee.java
│   └── Gender.java
├── Models
│   ├── EmployeeCreateRequest.java
│   └── EmployeeResponse.java
├── Repositories
│   └── EmployeeRepository.java
├── Services
│   └── EmployeeService.java

Co jsem se naučil:

  • Lazy v Java Stream API ( Vše se ve streamu zavolá až po .toList() ) např.:
    public List<EmployeeResponse> listAll()
    {
        return employeeRepository
            .findAll()
            .stream()
            .map(e-> modelMapper.map(e, EmployeeResponse.class))
            .toList(); //Vše se krásně sputí až zde.. Narozdíl od Javascriptu kde to tak není
    }
  • Ve Springbootu existuje tzv RestControllerAdvice, který je vlastně takový middleware pattern na error handling, kde se dá krásně error změnit na Json Response
@RestControllerAdvice
public class EmployeeRestControllerAdvice {

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleRuntimeException(RuntimeException e)
    {
        Map<String, Object> map = new HashMap<>();
        map.put("success", false);
        map.put("message", e.getMessage());
        return map;
    }
}
  • Monad Pattern v RestControllerAdvice:
    public EmployeeResponse getById(Integer id)
    {
        return employeeRepository.findById(id)
            .map(e-> modelMapper.map(e, EmployeeResponse.class))
            .orElseThrow(()-> new RuntimeException("Employee not found")); //<- Monad Pattern
    }

About

Škálovatelná Java Spring Boot Mikro-Servisní aplikace za pomocí Kubernetees a Dockeru. S frontendem v Next.js a Reactu

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published