Przejdź do treści
Backend

Spring Boot - Mikroserwisy w Javie od podstaw do produkcji

Opublikowano:
·4 min czytania·Autor: MDS Software Solutions Group

Spring Boot Mikroserwisy

backend

Spring Boot: Mikroserwisy w Javie od podstaw do produkcji

Spring Boot zrewolucjonizował sposób tworzenia aplikacji w Javie. Eliminując żmudną konfigurację XML i dostarczając gotowe do użycia komponenty, pozwala programistom skupić się na logice biznesowej zamiast na infrastrukturze. W tym artykule przeprowadzimy Cię przez cały ekosystem Spring Boot — od pierwszego endpointu REST, przez bezpieczeństwo i dostęp do danych, aż po wdrożenie mikroserwisów w Kubernetes.

Czym jest Spring Boot i ekosystem Spring?#

Spring Framework to jeden z najpopularniejszych frameworków do budowy aplikacji enterprise w Javie. Powstał jako alternatywa dla ciężkiego modelu EJB i opiera się na dwóch filarach: Inversion of Control (IoC) oraz Dependency Injection (DI).

Spring Boot to nadbudowa nad Spring Framework, która wprowadza:

  • Auto-konfigurację — automatyczne wykrywanie i konfigurowanie komponentów
  • Embedded serwery — Tomcat, Jetty lub Undertow wbudowane w aplikację
  • Spring Boot Starters — gotowe zestawy zależności dla typowych scenariuszy
  • Spring Boot Actuator — monitoring i zarządzanie aplikacją
  • Spring Boot DevTools — automatyczny restart i hot-reload w trybie development

Ekosystem Spring obejmuje dziesiątki projektów, ale najważniejsze to:

  • Spring MVC — kontrolery REST i obsługa HTTP
  • Spring Data — uproszczony dostęp do baz danych
  • Spring Security — uwierzytelnianie i autoryzacja
  • Spring Cloud — narzędzia do budowy mikroserwisów
  • Spring Batch — przetwarzanie wsadowe

Auto-konfiguracja i Convention over Configuration#

Jednym z kluczowych mechanizmów Spring Boot jest auto-konfiguracja. Wystarczy dodać odpowiedni starter do pom.xml lub build.gradle, a Spring Boot automatycznie skonfiguruje wszystkie potrzebne beany.

// To jest wszystko, czego potrzebujesz, aby uruchomić aplikację Spring Boot
@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

Adnotacja @SpringBootApplication łączy trzy mechanizmy:

  • @Configuration — oznacza klasę jako źródło definicji beanów
  • @EnableAutoConfiguration — włącza auto-konfigurację
  • @ComponentScan — skanuje pakiety w poszukiwaniu komponentów

Konfiguracja aplikacji odbywa się przez plik application.yml:

server:
  port: 8080

spring:
  application:
    name: order-service
  datasource:
    url: jdbc:postgresql://localhost:5432/orders
    username: ${DB_USERNAME:admin}
    password: ${DB_PASSWORD:secret}
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    properties:
      hibernate:
        format_sql: true
        default_batch_fetch_size: 20

Spring Data JPA — dostęp do bazy danych#

Spring Data JPA eliminuje potrzebę pisania boilerplate'u dla operacji CRUD. Wystarczy zdefiniować interfejs repozytorium, a Spring wygeneruje implementację automatycznie.

Encja JPA#

@Entity
@Table(name = "orders")
public class Order {

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

    @Column(nullable = false)
    private String customerEmail;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @Column(nullable = false)
    private BigDecimal totalAmount;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;

    // konstruktory, gettery, settery
}

Repozytorium#

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {

    List<Order> findByCustomerEmailOrderByCreatedAtDesc(String email);

    @Query("SELECT o FROM Order o WHERE o.status = :status AND o.createdAt > :since")
    Page<Order> findRecentByStatus(
        @Param("status") OrderStatus status,
        @Param("since") LocalDateTime since,
        Pageable pageable
    );

    @Modifying
    @Query("UPDATE Order o SET o.status = :status WHERE o.id = :id")
    int updateStatus(@Param("id") Long id, @Param("status") OrderStatus status);

    @Query("""
        SELECT new com.example.dto.OrderSummary(
            o.status, COUNT(o), SUM(o.totalAmount)
        )
        FROM Order o
        WHERE o.createdAt BETWEEN :from AND :to
        GROUP BY o.status
        """)
    List<OrderSummary> getOrderSummary(
        @Param("from") LocalDateTime from,
        @Param("to") LocalDateTime to
    );
}

Spring Data automatycznie generuje zapytania SQL na podstawie nazw metod (findByCustomerEmailOrderByCreatedAtDesc), a dla bardziej złożonych przypadków pozwala na definiowanie zapytań JPQL za pomocą adnotacji @Query.

REST Controllers i projektowanie API#

Spring MVC zapewnia elegancki model tworzenia kontrolerów REST. Ważne jest stosowanie dobrych praktyk projektowania API: odpowiednie kody HTTP, walidacja danych wejściowych i spójne odpowiedzi błędów.

@RestController
@RequestMapping("/api/v1/orders")
@Validated
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping
    public ResponseEntity<Page<OrderResponse>> getOrders(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(required = false) OrderStatus status) {

        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        Page<OrderResponse> orders = orderService.getOrders(status, pageable);
        return ResponseEntity.ok(orders);
    }

    @GetMapping("/{id}")
    public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
        return ResponseEntity.ok(orderService.getOrderById(id));
    }

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
            @Valid @RequestBody CreateOrderRequest request) {
        OrderResponse created = orderService.createOrder(request);
        URI location = URI.create("/api/v1/orders/" + created.id());
        return ResponseEntity.created(location).body(created);
    }

    @PatchMapping("/{id}/status")
    public ResponseEntity<OrderResponse> updateStatus(
            @PathVariable Long id,
            @Valid @RequestBody UpdateStatusRequest request) {
        return ResponseEntity.ok(orderService.updateStatus(id, request));
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void cancelOrder(@PathVariable Long id) {
        orderService.cancelOrder(id);
    }
}

Globalna obsługa błędów#

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        Map<String, String> fieldErrors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .collect(Collectors.toMap(
                FieldError::getField,
                FieldError::getDefaultMessage,
                (a, b) -> a
            ));

        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Validation failed",
            LocalDateTime.now(),
            fieldErrors
        );
        return ResponseEntity.badRequest().body(error);
    }
}

Spring Security — JWT i OAuth2#

Spring Security to kompleksowy framework zabezpieczeń. Nowoczesne aplikacje mikroserwisowe najczęściej korzystają z tokenów JWT lub protokołu OAuth2.

Konfiguracja bezpieczeństwa z JWT#

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;

    public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
        this.jwtAuthFilter = jwtAuthFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Filtr JWT#

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    public JwtAuthFilter(JwtService jwtService, UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain filterChain) throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = authHeader.substring(7);
        String username = jwtService.extractUsername(token);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (jwtService.isTokenValid(token, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                    );
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Spring Cloud — infrastruktura mikroserwisów#

Spring Cloud dostarcza zestaw narzędzi niezbędnych do budowy rozproszonych systemów mikroserwisowych. Kluczowe komponenty to Service Discovery, Config Server i API Gateway.

Eureka — Service Discovery#

Eureka pozwala mikroserwisem rejestrować się i odnajdywać nawzajem bez hardkodowania adresów.

Serwer Eureka:

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(DiscoveryServerApplication.class, args);
    }
}
# application.yml serwera Eureka
server:
  port: 8761

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false

Klient (mikroserwis):

spring:
  application:
    name: order-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

Spring Cloud Config Server#

Centralny serwer konfiguracji pozwala na zarządzanie konfiguracją wszystkich mikroserwisów z jednego miejsca (np. repozytorium Git):

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/my-org/config-repo
          default-label: main
          search-paths: '{application}'

Spring Cloud Gateway#

Gateway pełni rolę centralnego punktu wejścia do systemu mikroserwisów, obsługując routing, load balancing i filtrowanie żądań:

spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/v1/orders/**
          filters:
            - name: CircuitBreaker
              args:
                name: orderServiceCB
                fallbackUri: forward:/fallback/orders
            - name: RequestRateLimiter
              args:
                redis-rate-limiter:
                  replenishRate: 10
                  burstCapacity: 20

        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/api/v1/products/**

Spring Boot Actuator — monitoring i zarządzanie#

Actuator udostępnia szereg endpointów do monitorowania stanu aplikacji, co jest niezbędne w środowisku produkcyjnym.

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when_authorized
      probes:
        enabled: true
  metrics:
    tags:
      application: ${spring.application.name}

Niestandardowy Health Indicator#

@Component
public class ExternalApiHealthIndicator implements HealthIndicator {

    private final RestTemplate restTemplate;

    public ExternalApiHealthIndicator(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public Health health() {
        try {
            ResponseEntity<String> response =
                restTemplate.getForEntity("https://api.external-service.com/health", String.class);
            if (response.getStatusCode().is2xxSuccessful()) {
                return Health.up()
                    .withDetail("externalApi", "Available")
                    .withDetail("responseTime", "OK")
                    .build();
            }
            return Health.down().withDetail("externalApi", "Unhealthy").build();
        } catch (Exception e) {
            return Health.down()
                .withDetail("externalApi", "Unavailable")
                .withException(e)
                .build();
        }
    }
}

Spring Profiles — zarządzanie środowiskami#

Profile pozwalają na definiowanie różnych konfiguracji dla różnych środowisk: development, staging, production.

# application.yml — konfiguracja wspólna
spring:
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}

---
# application-dev.yml
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:h2:mem:testdb
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
  h2:
    console:
      enabled: true

logging:
  level:
    com.example: DEBUG
    org.hibernate.SQL: DEBUG

---
# application-prod.yml
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

logging:
  level:
    com.example: WARN

Aktywacja profilu odbywa się przez zmienną środowiskową lub parametr uruchomienia:

java -jar app.jar --spring.profiles.active=prod
# lub
SPRING_PROFILES_ACTIVE=prod java -jar app.jar

Testowanie — MockMvc i Testcontainers#

Spring Boot oferuje bogaty ekosystem testowy. Testy integracyjne z MockMvc pozwalają testować kontrolery bez uruchamiania serwera HTTP, a Testcontainers umożliwiają testowanie z prawdziwą bazą danych w Dockerze.

Testy kontrolerów z MockMvc#

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderService orderService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void shouldCreateOrder() throws Exception {
        CreateOrderRequest request = new CreateOrderRequest(
            "customer@example.com",
            List.of(new OrderItemRequest(1L, 2))
        );

        OrderResponse response = new OrderResponse(
            1L, "customer@example.com", OrderStatus.CREATED,
            new BigDecimal("199.98"), LocalDateTime.now()
        );

        when(orderService.createOrder(any())).thenReturn(response);

        mockMvc.perform(post("/api/v1/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.status").value("CREATED"))
            .andExpect(header().exists("Location"));
    }

    @Test
    void shouldReturn400ForInvalidRequest() throws Exception {
        CreateOrderRequest request = new CreateOrderRequest("", List.of());

        mockMvc.perform(post("/api/v1/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.fieldErrors").exists());
    }
}

Testy integracyjne z Testcontainers#

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class OrderIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private OrderRepository orderRepository;

    @BeforeEach
    void setUp() {
        orderRepository.deleteAll();
    }

    @Test
    void shouldCreateAndRetrieveOrder() {
        CreateOrderRequest request = new CreateOrderRequest(
            "test@example.com",
            List.of(new OrderItemRequest(1L, 3))
        );

        ResponseEntity<OrderResponse> createResponse = restTemplate
            .postForEntity("/api/v1/orders", request, OrderResponse.class);

        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(createResponse.getBody()).isNotNull();
        Long orderId = createResponse.getBody().id();

        ResponseEntity<OrderResponse> getResponse = restTemplate
            .getForEntity("/api/v1/orders/" + orderId, OrderResponse.class);

        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getResponse.getBody().customerEmail()).isEqualTo("test@example.com");
    }
}

Docker i Kubernetes — wdrożenie produkcyjne#

Dockerfile z multi-stage build#

# Stage 1: Build
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradle/ gradle/
COPY gradlew build.gradle settings.gradle ./
RUN ./gradlew dependencies --no-daemon
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test

# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app

RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

COPY --from=builder /app/build/libs/*.jar app.jar

EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

Kubernetes Deployment#

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  labels:
    app: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: registry.example.com/order-service:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: "prod"
            - name: DB_HOST
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: host
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "1Gi"
              cpu: "1000m"
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 15
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP

Tuning wydajności#

Wydajność aplikacji Spring Boot zależy od wielu czynników. Oto najważniejsze obszary optymalizacji.

Pule połączeń z HikariCP#

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000
      max-lifetime: 1200000
      leak-detection-threshold: 60000

Cachowanie z Spring Cache#

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(Duration.ofMinutes(10))
            .recordStats());
        return manager;
    }
}

@Service
public class ProductService {

    @Cacheable(value = "products", key = "#id")
    public ProductResponse getProduct(Long id) {
        return productRepository.findById(id)
            .map(this::toResponse)
            .orElseThrow(() -> new ResourceNotFoundException("Product not found: " + id));
    }

    @CacheEvict(value = "products", key = "#id")
    public ProductResponse updateProduct(Long id, UpdateProductRequest request) {
        // aktualizacja produktu
    }

    @CacheEvict(value = "products", allEntries = true)
    @Scheduled(fixedRate = 3600000)
    public void evictAllProductsCache() {
        // automatyczne czyszczenie cache co godzinę
    }
}

Optymalizacja zapytań JPA#

// Problem N+1 — rozwiązanie z EntityGraph
@EntityGraph(attributePaths = {"items", "items.product"})
@Query("SELECT o FROM Order o WHERE o.customerEmail = :email")
List<Order> findOrdersWithItems(@Param("email") String email);

// Projekcje — pobieranie tylko potrzebnych kolumn
public interface OrderSummaryProjection {
    Long getId();
    String getCustomerEmail();
    OrderStatus getStatus();
    BigDecimal getTotalAmount();
}

@Query("SELECT o FROM Order o WHERE o.status = :status")
List<OrderSummaryProjection> findSummariesByStatus(@Param("status") OrderStatus status);

Kiedy wybrać Spring Boot?#

Spring Boot jest doskonałym wyborem, gdy:

  • Budujesz aplikacje enterprise — dojrzały ekosystem z tysiącami bibliotek
  • Potrzebujesz mikroserwisów — Spring Cloud zapewnia pełne wsparcie
  • Twój zespół zna Javę — krzywa nauki Spring Boot jest łagodna
  • Zależy Ci na wydajności — JVM oferuje znakomitą wydajność dla długotrwałych procesów
  • Potrzebujesz bezpieczeństwa enterprise — Spring Security to standard branżowy
  • Integracja z systemami legacy — Java ma najszerszą gamę konektorów

Rozważ alternatywy (Quarkus, Micronaut), jeśli priorytetem jest czas startu aplikacji lub zużycie pamięci w środowiskach serverless.

Podsumowanie#

Spring Boot to potężna platforma, która pokrywa praktycznie każdy aspekt budowy nowoczesnych aplikacji:

  • Auto-konfiguracja radykalnie redukuje boilerplate
  • Spring Data JPA upraszcza dostęp do danych
  • Spring Security zapewnia bezpieczeństwo na poziomie enterprise
  • Spring Cloud dostarcza infrastrukturę dla mikroserwisów
  • Actuator umożliwia monitoring produkcyjny
  • Testcontainers pozwalają na realistyczne testy integracyjne

Kluczem do sukcesu jest znajomość ekosystemu i stosowanie najlepszych praktyk od samego początku projektu.

Potrzebujesz wsparcia przy projektach Java?#

W MDS Software Solutions Group specjalizujemy się w budowie aplikacji enterprise z wykorzystaniem Spring Boot i mikroserwisów. Oferujemy:

  • Projektowanie architektury mikroserwisowej
  • Rozwój aplikacji Spring Boot od podstaw
  • Migrację monolitów do mikroserwisów
  • Wdrożenia w Docker i Kubernetes
  • Audyt wydajności i bezpieczeństwa
  • Wsparcie techniczne i szkolenia

Skontaktuj się z nami, aby omówić Twój projekt!

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

Spring Boot - Mikroserwisy w Javie od podstaw do produkcji | MDS Software Solutions Group | MDS Software Solutions Group