過剰設計にならないための判断基準
過剰設計にならないための判断基準
Section titled “過剰設計にならないための判断基準”過剰設計を避け、適切な設計を行うための判断基準を詳しく解説します。
判断基準1: トランザクション境界の明確化
Section titled “判断基準1: トランザクション境界の明確化”シンプルなケース
Section titled “シンプルなケース”条件:
- 単一のデータベース操作のみ
- 外部API呼び出しがない
- 処理時間が短い(1秒以内)
実装:
// ✅ シンプルな実装で十分@Servicepublic class UserService { @Autowired private UserRepository userRepository;
@Transactional public User createUser(UserData userData) { // 単一のデータベース操作のみ return userRepository.save(new User(userData)); }}判断基準:
- トランザクションが必要: データの整合性を保つ必要がある
- Outboxパターンは不要: 外部API呼び出しがない
- 非同期処理は不要: 処理時間が短い
複雑なケース
Section titled “複雑なケース”条件:
- 複数のデータベース操作
- 外部API呼び出しがある
- 処理時間が長い(5秒以上)
実装:
// ✅ 適切な設計が必要@Servicepublic class OrderService { @Autowired private OrderRepository orderRepository;
@Autowired private OutboxRepository outboxRepository;
@Transactional public Order createOrder(OrderData orderData) { // トランザクション内でのデータベース操作のみ Order order = orderRepository.save(new Order(orderData));
// Outboxパターンを使用 OutboxEvent event = new OutboxEvent(); event.setEventType("PAYMENT_CHARGE"); event.setAggregateId(order.getId()); event.setPayload(JSON.toJSONString(Map.of( "orderId", order.getId(), "amount", orderData.getAmount() ))); event.setStatus("PENDING"); outboxRepository.save(event);
return order; }}判断基準:
- Outboxパターンが必要: 外部API呼び出しがある
- 非同期処理が必要: 処理時間が長い
判断基準2: Serverless環境での設計
Section titled “判断基準2: Serverless環境での設計”Vercel環境
Section titled “Vercel環境”条件:
- 実行時間が短い(10秒以内)
- メモリ使用量が少ない(512MB以内)
- ステートレスな処理
実装:
// ✅ Vercel環境に適した実装@RestControllerpublic class UserController { @Autowired private UserService userService;
@GetMapping("/users/{id}") public ResponseEntity<User> getUser(@PathVariable Long id) { // 短時間の同期処理 User user = userService.findById(id); return ResponseEntity.ok(user); }}判断基準:
- 同期処理で十分: 実行時間が短い
- Outboxパターンは不要: 外部API呼び出しがない
Render.com環境
Section titled “Render.com環境”条件:
- 実行時間が長い(5分以上)
- メモリ使用量が多い(1GB以上)
- ステートフルな処理
実装:
// ✅ Render.com環境に適した実装@RestControllerpublic class ReportController { @Autowired private ReportService reportService;
@PostMapping("/reports/generate") public ResponseEntity<ReportResponse> generateReport(@RequestBody ReportRequest request) { // 長時間実行される処理は非同期に String reportId = UUID.randomUUID().toString(); reportService.generateReportAsync(reportId, request); return ResponseEntity.accepted() .body(new ReportResponse(reportId, "PROCESSING")); }}判断基準:
- 非同期処理が必要: 実行時間が長い
- 常駐プロセスが必要: 長時間実行される処理
判断基準3: エラーハンドリングのレベル
Section titled “判断基準3: エラーハンドリングのレベル”シンプルなエラーハンドリング
Section titled “シンプルなエラーハンドリング”条件:
- エラーが発生する可能性が低い
- エラーの影響が小さい
- リトライが不要
実装:
// ✅ シンプルなエラーハンドリングで十分@Servicepublic class UserService { @Autowired private UserRepository userRepository;
public User getUser(Long id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException(id)); }}判断基準:
- 基本的なエラーハンドリングで十分: エラーの影響が小さい
- リトライは不要: エラーが発生する可能性が低い
複雑なエラーハンドリング
Section titled “複雑なエラーハンドリング”条件:
- エラーが発生する可能性が高い
- エラーの影響が大きい
- リトライが必要
実装:
// ✅ 複雑なエラーハンドリングが必要@Servicepublic class PaymentService { @Autowired private PaymentApiClient paymentApiClient;
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000)) public PaymentResult chargePayment(Long orderId, BigDecimal amount) { try { return paymentApiClient.chargePayment(orderId, amount); } catch (TimeoutException e) { // タイムアウトエラーはリトライ throw new RetryableException("Payment timeout", e); } catch (PaymentException e) { // 決済エラーはリトライしない throw new NonRetryableException("Payment failed", e); } }}判断基準:
- リトライが必要: エラーが発生する可能性が高い
- エラーの分類が必要: リトライ可能なエラーと不可能なエラーを区別
判断基準4: キャッシュの使用
Section titled “判断基準4: キャッシュの使用”キャッシュが不要なケース
Section titled “キャッシュが不要なケース”条件:
- データの更新頻度が高い
- データの整合性が重要
- キャッシュの効果が小さい
実装:
// ✅ キャッシュは不要@Servicepublic class OrderService { @Autowired private OrderRepository orderRepository;
public Order getOrder(Long id) { // キャッシュなしで直接データベースから取得 return orderRepository.findById(id) .orElseThrow(() -> new OrderNotFoundException(id)); }}判断基準:
- キャッシュは不要: データの更新頻度が高い
- 直接データベースアクセス: データの整合性が重要
キャッシュが必要なケース
Section titled “キャッシュが必要なケース”条件:
- データの更新頻度が低い
- 読み取り頻度が高い
- キャッシュの効果が大きい
実装:
// ✅ キャッシュが必要@Servicepublic class ProductService { @Autowired private ProductRepository productRepository;
@Cacheable(value = "products", key = "#id") public Product getProduct(Long id) { // キャッシュから取得、なければデータベースから取得 return productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException(id)); }
@CacheEvict(value = "products", key = "#product.id") @Transactional public Product updateProduct(Product product) { // 更新時にキャッシュを無効化 return productRepository.save(product); }}判断基準:
- キャッシュが必要: 読み取り頻度が高い
- キャッシュ無効化が必要: 更新時にキャッシュを無効化
判断基準5: マイクロサービス化
Section titled “判断基準5: マイクロサービス化”モノリスで十分なケース
Section titled “モノリスで十分なケース”条件:
- チームサイズが小規模(10人以下)
- 開発速度が最重要
- シンプルなアプリケーション
判断基準:
- モノリスで十分: チームサイズが小規模
- マイクロサービス化は不要: 開発速度が最重要
マイクロサービス化が必要なケース
Section titled “マイクロサービス化が必要なケース”条件:
- チームサイズが大規模(10人以上)
- 独立したデプロイが必要
- 異なる技術スタックが必要
判断基準:
- マイクロサービス化が必要: チームサイズが大規模
- 独立したデプロイが必要: サービスごとに独立してデプロイ
過剰設計にならないための判断基準のポイント:
- トランザクション境界: シンプルなケースではシンプルな実装で十分
- Serverless環境: Vercel環境では同期処理、Render.com環境では非同期処理
- エラーハンドリング: エラーの影響に応じて適切なレベルを選択
- キャッシュ: データの特性に応じてキャッシュを使用
- マイクロサービス化: チームサイズと要件に応じて判断
適切な判断基準により、過剰設計を避け、効率的なシステムを構築できます。