Skip to content

第38章:API 設計とサービス連携

本章では、基幹業務システムにおける API 設計の原則と、サービス間連携のパターンについて解説します。RESTful API の設計、サービス間通信の方式、API ゲートウェイの活用、そしてインテグレーションテストの実践方法を学びます。


38.1 API 設計の原則

RESTful API の設計

REST(Representational State Transfer)は、Web API 設計の標準的なアーキテクチャスタイルです。

uml diagram

HTTP メソッドとリソース操作

HTTP メソッド 操作 冪等性 安全性 使用例
GET 取得 Yes Yes リソースの参照
POST 作成 No No 新規リソース作成
PUT 置換 Yes No リソース全体の更新
PATCH 部分更新 No No リソースの一部更新
DELETE 削除 Yes No リソースの削除

リソース指向設計

API はリソース(名詞)を中心に設計し、操作は HTTP メソッドで表現します。

uml diagram

基幹業務システムの API エンドポイント設計

uml diagram

OpenAPI 定義例
openapi: 3.0.3
info:
  title: 販売管理 API
  version: 1.0.0
  description: 基幹業務システム - 販売管理 API

servers:
  - url: https://api.example.com/api/v1/sales
    description: Production server

paths:
  /orders:
    get:
      summary: 受注一覧取得
      operationId: getOrders
      parameters:
        - name: customerId
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
            enum: [DRAFT, CONFIRMED, SHIPPED, COMPLETED]
        - name: fromDate
          in: query
          schema:
            type: string
            format: date
        - name: toDate
          in: query
          schema:
            type: string
            format: date
        - name: page
          in: query
          schema:
            type: integer
            default: 0
        - name: size
          in: query
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderListResponse'

    post:
      summary: 受注登録
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: 作成成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderResponse'
        '400':
          description: バリデーションエラー
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetail'

  /orders/{orderId}:
    get:
      summary: 受注詳細取得
      operationId: getOrder
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderResponse'
        '404':
          description: 受注が見つからない

components:
  schemas:
    OrderResponse:
      type: object
      properties:
        orderId:
          type: string
        customerId:
          type: string
        customerName:
          type: string
        orderDate:
          type: string
          format: date
        status:
          type: string
          enum: [DRAFT, CONFIRMED, SHIPPED, COMPLETED]
        totalAmount:
          type: number
        taxAmount:
          type: number
        lines:
          type: array
          items:
            $ref: '#/components/schemas/OrderLineResponse'
        _links:
          $ref: '#/components/schemas/Links'

    CreateOrderRequest:
      type: object
      required:
        - customerId
        - orderDate
        - lines
      properties:
        customerId:
          type: string
        orderDate:
          type: string
          format: date
        requestedDeliveryDate:
          type: string
          format: date
        lines:
          type: array
          items:
            $ref: '#/components/schemas/CreateOrderLineRequest'
          minItems: 1

    ProblemDetail:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string

バージョニング戦略

API の互換性を維持しながら進化させるためのバージョニング戦略を検討します。

uml diagram

推奨:URI パスバージョニング

uml diagram


38.2 サービス間通信

同期通信(REST / gRPC)

uml diagram

gRPC 定義例
syntax = "proto3";

package sales.v1;

option java_package = "com.example.sales.grpc";
option java_multiple_files = true;

// 在庫サービス
service InventoryService {
  // 在庫照会
  rpc GetStock(GetStockRequest) returns (StockResponse);

  // 在庫引当
  rpc AllocateStock(AllocateStockRequest) returns (AllocateStockResponse);

  // 引当解除
  rpc ReleaseStock(ReleaseStockRequest) returns (ReleaseStockResponse);
}

message GetStockRequest {
  string product_id = 1;
  string warehouse_id = 2;
}

message StockResponse {
  string product_id = 1;
  string warehouse_id = 2;
  int32 quantity = 3;
  int32 allocated_quantity = 4;
  int32 available_quantity = 5;
}

message AllocateStockRequest {
  string product_id = 1;
  string warehouse_id = 2;
  int32 quantity = 3;
  string order_id = 4;
}

message AllocateStockResponse {
  bool success = 1;
  string allocation_id = 2;
  string error_message = 3;
}

非同期通信(メッセージキュー)

uml diagram

Java 実装例(Spring AMQP)
// メッセージ送信
@Service
public class OrderMessagePublisher {
    private final RabbitTemplate rabbitTemplate;

    public void publishOrderCreated(OrderCreatedEvent event) {
        rabbitTemplate.convertAndSend(
            "sales.exchange",
            "order.created",
            event
        );
    }

    public void publishSalesCompleted(SalesCompletedEvent event) {
        rabbitTemplate.convertAndSend(
            "sales.topic.exchange",
            "sales.completed",
            event
        );
    }
}

// メッセージ受信
@Component
public class OrderMessageListener {

    @RabbitListener(queues = "inventory.order.queue")
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 在庫引当処理
        log.info("Received order: {}", event.orderId());
        inventoryService.allocate(event);
    }
}

// 設定
@Configuration
public class RabbitMQConfig {

    @Bean
    public TopicExchange salesTopicExchange() {
        return new TopicExchange("sales.topic.exchange");
    }

    @Bean
    public Queue accountingQueue() {
        return new Queue("accounting.sales.queue", true);
    }

    @Bean
    public Binding accountingBinding(Queue accountingQueue,
                                      TopicExchange salesTopicExchange) {
        return BindingBuilder.bind(accountingQueue)
            .to(salesTopicExchange)
            .with("sales.*");
    }
}

サーキットブレーカーパターン

サービス間通信の障害を検知し、障害の連鎖を防ぐパターンです。

uml diagram

uml diagram

Java 実装例(Resilience4j)
// 設定
@Configuration
public class CircuitBreakerConfig {

    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)  // 失敗率50%でOpen
            .waitDurationInOpenState(Duration.ofSeconds(30))
            .permittedNumberOfCallsInHalfOpenState(3)
            .slidingWindowType(SlidingWindowType.COUNT_BASED)
            .slidingWindowSize(10)
            .build();

        return CircuitBreakerRegistry.of(config);
    }
}

// サービス
@Service
public class InventoryServiceClient {
    private final RestTemplate restTemplate;
    private final CircuitBreaker circuitBreaker;

    public InventoryServiceClient(RestTemplate restTemplate,
                                   CircuitBreakerRegistry registry) {
        this.restTemplate = restTemplate;
        this.circuitBreaker = registry.circuitBreaker("inventoryService");
    }

    public StockResponse getStock(String productId) {
        Supplier<StockResponse> supplier = CircuitBreaker
            .decorateSupplier(circuitBreaker, () -> {
                return restTemplate.getForObject(
                    "/api/v1/inventory/{productId}",
                    StockResponse.class,
                    productId
                );
            });

        return Try.ofSupplier(supplier)
            .recover(CallNotPermittedException.class,
                e -> getFallbackStock(productId))
            .recover(Exception.class,
                e -> getFallbackStock(productId))
            .get();
    }

    private StockResponse getFallbackStock(String productId) {
        // フォールバック: キャッシュから取得または推定値
        return new StockResponse(productId, 0, 0, 0, "UNKNOWN");
    }
}

// アノテーションベース
@Service
public class AccountingServiceClient {

    @CircuitBreaker(name = "accountingService", fallbackMethod = "fallback")
    @Retry(name = "accountingService")
    @TimeLimiter(name = "accountingService")
    public CompletableFuture<JournalResponse> createJournal(
            CreateJournalRequest request) {
        return CompletableFuture.supplyAsync(() -> {
            return restTemplate.postForObject(
                "/api/v1/accounting/journals",
                request,
                JournalResponse.class
            );
        });
    }

    public CompletableFuture<JournalResponse> fallback(
            CreateJournalRequest request, Throwable t) {
        log.warn("Fallback for createJournal: {}", t.getMessage());
        // 仕訳をキューに保存して後で再試行
        pendingJournalQueue.add(request);
        return CompletableFuture.completedFuture(
            JournalResponse.pending(request.getCorrelationId())
        );
    }
}

38.3 API ゲートウェイ

API ゲートウェイは、クライアントと複数のバックエンドサービスの間に位置し、横断的な関心事を一元的に処理します。

uml diagram

認証・認可の一元化

uml diagram

Java 実装例(Spring Security + JWT)
// JWT フィルター
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain filterChain)
            throws ServletException, IOException {

        String token = extractToken(request);

        if (token != null && tokenProvider.validateToken(token)) {
            Authentication auth = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }

        filterChain.doFilter(request, response);
    }

    private String extractToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

// JWT プロバイダー
@Component
public class JwtTokenProvider {
    @Value("${jwt.secret}")
    private String jwtSecret;

    @Value("${jwt.expiration}")
    private long jwtExpiration;

    public String generateToken(Authentication authentication) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();

        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .claim("roles", userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList()))
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    public Authentication getAuthentication(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody();

        List<String> roles = claims.get("roles", List.class);
        List<GrantedAuthority> authorities = roles.stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());

        User principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(
            principal, token, authorities
        );
    }
}

レート制限とスロットリング

uml diagram

Spring Cloud Gateway 設定例
# application.yml
spring:
  cloud:
    gateway:
      routes:
        - id: sales-service
          uri: lb://sales-service
          predicates:
            - Path=/api/v1/sales/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 100
                redis-rate-limiter.burstCapacity: 200
                key-resolver: "#{@userKeyResolver}"
            - name: CircuitBreaker
              args:
                name: salesCircuitBreaker
                fallbackUri: forward:/fallback/sales

        - id: accounting-service
          uri: lb://accounting-service
          predicates:
            - Path=/api/v1/accounting/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 50
                redis-rate-limiter.burstCapacity: 100

      default-filters:
        - name: Retry
          args:
            retries: 3
            statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
            methods: GET
            backoff:
              firstBackoff: 100ms
              maxBackoff: 500ms
              factor: 2
// Key Resolver(ユーザー単位のレート制限)
@Configuration
public class RateLimiterConfig {

    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> {
            String userId = exchange.getRequest()
                .getHeaders()
                .getFirst("X-User-Id");
            return Mono.just(userId != null ? userId : "anonymous");
        };
    }

    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> {
            String apiKey = exchange.getRequest()
                .getHeaders()
                .getFirst("X-API-Key");
            return Mono.just(apiKey != null ? apiKey : "default");
        };
    }
}

ログ集約とモニタリング

uml diagram

Java 実装例(Micrometer + OpenTelemetry)
// トレーシング設定
@Configuration
public class TracingConfig {

    @Bean
    public Tracer tracer() {
        return GlobalOpenTelemetry.getTracer("sales-service");
    }
}

// カスタムメトリクス
@Component
public class OrderMetrics {
    private final MeterRegistry meterRegistry;
    private final Counter orderCreatedCounter;
    private final Timer orderProcessingTimer;

    public OrderMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;

        this.orderCreatedCounter = Counter.builder("orders.created")
            .description("Number of orders created")
            .tag("service", "sales")
            .register(meterRegistry);

        this.orderProcessingTimer = Timer.builder("orders.processing.time")
            .description("Order processing time")
            .tag("service", "sales")
            .register(meterRegistry);
    }

    public void recordOrderCreated(String status) {
        orderCreatedCounter.increment();
        meterRegistry.counter("orders.created.by.status",
            "status", status).increment();
    }

    public void recordProcessingTime(long durationMs) {
        orderProcessingTimer.record(Duration.ofMillis(durationMs));
    }
}

// 構造化ログ
@Slf4j
@Service
public class OrderService {

    public Order createOrder(CreateOrderRequest request) {
        MDC.put("orderId", request.getOrderId());
        MDC.put("customerId", request.getCustomerId());

        try {
            log.info("Creating order", kv("action", "create_order_start"));

            Order order = processOrder(request);

            log.info("Order created successfully",
                kv("action", "create_order_complete"),
                kv("totalAmount", order.getTotalAmount()));

            return order;
        } catch (Exception e) {
            log.error("Failed to create order",
                kv("action", "create_order_failed"),
                kv("error", e.getMessage()), e);
            throw e;
        } finally {
            MDC.clear();
        }
    }
}

38.4 API インテグレーションテスト

テストコンテナによる統合テスト環境

uml diagram

Java 実装例
// テストコンテナ設定
@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public abstract class IntegrationTestBase {

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

    @Container
    static RabbitMQContainer rabbitmq = new RabbitMQContainer("rabbitmq:3.12-management");

    @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);
        registry.add("spring.rabbitmq.host", rabbitmq::getHost);
        registry.add("spring.rabbitmq.port", rabbitmq::getAmqpPort);
    }

    @Autowired
    protected TestRestTemplate restTemplate;

    @Autowired
    protected JdbcTemplate jdbcTemplate;

    @BeforeEach
    void setUp() {
        // テストデータのクリーンアップ
        cleanupTestData();
    }

    protected void cleanupTestData() {
        jdbcTemplate.execute("TRUNCATE TABLE 受注明細 CASCADE");
        jdbcTemplate.execute("TRUNCATE TABLE 受注 CASCADE");
    }
}

REST API エンドポイントのテスト

uml diagram

Java 実装例
// 受注API統合テスト
class OrderApiIntegrationTest extends IntegrationTestBase {

    @Autowired
    private CustomerRepository customerRepository;

    @Autowired
    private ProductRepository productRepository;

    private String authToken;
    private Customer testCustomer;
    private Product testProduct;

    @BeforeEach
    void setUpTestData() {
        // 認証トークン取得
        authToken = getAuthToken("test-user", "password");

        // テストデータ準備
        testCustomer = customerRepository.save(
            new Customer("CUS-001", "テスト顧客", "test@example.com")
        );
        testProduct = productRepository.save(
            new Product("PRD-001", "テスト商品", new BigDecimal("1000"))
        );
    }

    @Test
    @DisplayName("受注登録: 正常系")
    void createOrder_Success() {
        // Given
        CreateOrderRequest request = CreateOrderRequest.builder()
            .customerId(testCustomer.getCustomerId())
            .orderDate(LocalDate.now())
            .lines(List.of(
                OrderLineRequest.builder()
                    .productId(testProduct.getProductId())
                    .quantity(10)
                    .build()
            ))
            .build();

        // When
        ResponseEntity<OrderResponse> response = restTemplate.exchange(
            "/api/v1/sales/orders",
            HttpMethod.POST,
            new HttpEntity<>(request, createAuthHeaders()),
            OrderResponse.class
        );

        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().getOrderId()).isNotBlank();
        assertThat(response.getBody().getStatus()).isEqualTo("DRAFT");
        assertThat(response.getBody().getTotalAmount())
            .isEqualByComparingTo(new BigDecimal("10000"));

        // データベース状態確認
        Order savedOrder = orderRepository.findById(
            response.getBody().getOrderId()
        ).orElseThrow();
        assertThat(savedOrder.getCustomerId()).isEqualTo(testCustomer.getCustomerId());
        assertThat(savedOrder.getLines()).hasSize(1);
    }

    @Test
    @DisplayName("受注登録: バリデーションエラー")
    void createOrder_ValidationError() {
        // Given - 明細なしのリクエスト
        CreateOrderRequest request = CreateOrderRequest.builder()
            .customerId(testCustomer.getCustomerId())
            .orderDate(LocalDate.now())
            .lines(List.of()) // 空の明細
            .build();

        // When
        ResponseEntity<ProblemDetail> response = restTemplate.exchange(
            "/api/v1/sales/orders",
            HttpMethod.POST,
            new HttpEntity<>(request, createAuthHeaders()),
            ProblemDetail.class
        );

        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        assertThat(response.getBody().getTitle()).isEqualTo("Validation Error");
    }

    @Test
    @DisplayName("受注一覧取得: ページング")
    void getOrders_Paging() {
        // Given - 複数の受注を作成
        for (int i = 0; i < 25; i++) {
            createTestOrder("ORD-" + String.format("%03d", i));
        }

        // When
        ResponseEntity<PagedResponse<OrderSummary>> response = restTemplate.exchange(
            "/api/v1/sales/orders?page=0&size=10",
            HttpMethod.GET,
            new HttpEntity<>(createAuthHeaders()),
            new ParameterizedTypeReference<>() {}
        );

        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody().getContent()).hasSize(10);
        assertThat(response.getBody().getTotalElements()).isEqualTo(25);
        assertThat(response.getBody().getTotalPages()).isEqualTo(3);
    }

    private HttpHeaders createAuthHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(authToken);
        headers.setContentType(MediaType.APPLICATION_JSON);
        return headers;
    }
}

サービス間連携テスト

uml diagram

Java 実装例(WireMock)
// WireMockを使用したサービス間連携テスト
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
class OrderServiceWithInventoryTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Value("${wiremock.server.port}")
    private int wireMockPort;

    @BeforeEach
    void setUp() {
        // 在庫サービスのモック設定
        stubFor(get(urlPathMatching("/api/v1/inventory/.*"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {
                        "productId": "PRD-001",
                        "quantity": 100,
                        "allocatedQuantity": 0,
                        "availableQuantity": 100
                    }
                    """)));

        stubFor(post(urlPathMatching("/api/v1/inventory/allocate"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {
                        "success": true,
                        "allocationId": "ALLOC-001"
                    }
                    """)));
    }

    @Test
    @DisplayName("受注確定: 在庫引当成功")
    void confirmOrder_InventoryAllocated() {
        // Given
        String orderId = createDraftOrder();

        // When
        ResponseEntity<OrderResponse> response = restTemplate.exchange(
            "/api/v1/sales/orders/{orderId}/confirm",
            HttpMethod.POST,
            new HttpEntity<>(createAuthHeaders()),
            OrderResponse.class,
            orderId
        );

        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody().getStatus()).isEqualTo("CONFIRMED");

        // 在庫サービスへの呼び出しを検証
        verify(postRequestedFor(urlPathEqualTo("/api/v1/inventory/allocate"))
            .withRequestBody(matchingJsonPath("$.productId", equalTo("PRD-001")))
            .withRequestBody(matchingJsonPath("$.quantity", equalTo("10"))));
    }

    @Test
    @DisplayName("受注確定: 在庫不足")
    void confirmOrder_InsufficientStock() {
        // Given - 在庫不足のレスポンスを設定
        stubFor(post(urlPathMatching("/api/v1/inventory/allocate"))
            .willReturn(aResponse()
                .withStatus(200)
                .withBody("""
                    {
                        "success": false,
                        "errorMessage": "Insufficient stock"
                    }
                    """)));

        String orderId = createDraftOrder();

        // When
        ResponseEntity<ProblemDetail> response = restTemplate.exchange(
            "/api/v1/sales/orders/{orderId}/confirm",
            HttpMethod.POST,
            new HttpEntity<>(createAuthHeaders()),
            ProblemDetail.class,
            orderId
        );

        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
        assertThat(response.getBody().getDetail()).contains("在庫不足");
    }

    @Test
    @DisplayName("受注確定: 在庫サービスタイムアウト")
    void confirmOrder_InventoryServiceTimeout() {
        // Given - タイムアウトをシミュレート
        stubFor(post(urlPathMatching("/api/v1/inventory/allocate"))
            .willReturn(aResponse()
                .withFixedDelay(5000) // 5秒遅延
                .withStatus(200)));

        String orderId = createDraftOrder();

        // When
        ResponseEntity<ProblemDetail> response = restTemplate.exchange(
            "/api/v1/sales/orders/{orderId}/confirm",
            HttpMethod.POST,
            new HttpEntity<>(createAuthHeaders()),
            ProblemDetail.class,
            orderId
        );

        // Then - サーキットブレーカーによるフォールバック
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
    }
}

38.5 まとめ

本章では、API 設計とサービス連携のパターンについて解説しました。

学んだこと

  1. API 設計の原則

  2. RESTful API の制約と設計原則

  3. リソース指向設計(名詞ベース、HTTP メソッド)
  4. バージョニング戦略(URI パス推奨)

  5. サービス間通信

  6. 同期通信(REST、gRPC)の使い分け

  7. 非同期通信(メッセージキュー)
  8. サーキットブレーカーによる障害対策

  9. API ゲートウェイ

  10. 認証・認可の一元化(JWT)

  11. レート制限とスロットリング
  12. ログ集約とモニタリング

  13. API インテグレーションテスト

  14. テストコンテナによる統合テスト環境

  15. REST API エンドポイントのテスト
  16. サービス間連携テスト(WireMock)

API 設計チェックリスト

  • リソースは名詞(複数形)で命名されているか
  • HTTP メソッドが正しく使用されているか
  • エラーレスポンスは Problem Detail 形式か
  • バージョニング戦略が決定されているか
  • 認証・認可が適切に実装されているか
  • レート制限が設定されているか
  • サーキットブレーカーが導入されているか
  • 統合テストが整備されているか

次章の予告

第39章では、データ連携の実装パターンについて解説します。バッチ連携、リアルタイム連携、連携テーブルの設計など、具体的な実装パターンを学びます。