技術的負債で生じる「クラフト」を管理せよ! ケーススタディで学ぶ4分類とその対処法

技術的負債で生じる「クラフト」を管理せよ! ケーススタディで学ぶ4分類とその対処法

はじめまして。流しのアーキテクトを生業としている、株式会社ウルフチーフの川島@kawasimaです。私はSIerで20年、主にアーキテクトとしてさまざまなプロジェクトに関わってきました。現在は独立し、データモデリングを中心に、多くの企業の設計や技術者育成を支援しています。

数多くの現場を渡り歩く中で、プロジェクトを停滞させる「ある共通項」が見えてきました。それが、技術的負債に伴って静かに蓄積する「クラフト」です。本稿ではこの「クラフト」の正体を突き止め、パターンごとの対処法を具体的に紐解いていきます。

メンテナンスコストを増加させる「クラフト」

技術的負債という言葉は、しばしば手抜きなコードや読みにくいコードを指して使われます。しかしそのたびに、「本来の意味とは違う」という指摘が入るのをよく見かけないでしょうか?

そもそもこの技術的負債の起源は、ウォード・カニンガム*1の「最初にリリースするコードは負債を抱えるようなもので、少しの負債は開発を加速させるが、迅速にリファクタリングで返済される必要がある」という発言に由来します。開発速度と将来のリファクタリングを引き換えにする行為を、負債を負うことに例えたわけです。

この負債を負う選択をした結果、予測が外れると、理想とは異なるコードができてしまい、その後のメンテナンスに悪影響を及ぼしてしまいます。マーティン・ファウラー*2「技術的負債」のブログ記事で、内部品質の問題を「クラフト(cruft)」と呼びました

実は、クラフトは技術的負債以外のさまざまな要因でも発生します。例えば、明確な設計・コーディングルールが定まっていない状態で作られた「不統一なコード」です。コーディング規約やアーキテクチャの方針が曖昧なまま進められたプロジェクトでは、各開発者の経験や好みで異なるアプローチが採用され、全体の一貫性が失われがちです。

また、設計に問題があると認識していても、解決方法が分からなかったり、時間的制約から「動作するから良い」と判断して後回しにしたまま本番環境にデプロイされたコードもクラフトとなります。このような妥協は短期的には合理的に思えても、長期的にはシステムの複雑性を増大させてしまうのです。

さらに、ビジネス要件の急激な変化や予期せぬスケジュール短縮で十分な設計検討ができなかった場合も、クラフトが生まれやすい状況と言えます。

それが技術的負債に起因するかどうかに関係なく、一度クラフトが生まれると、解消されない限りメンテナンスコストを増加させ続けます。具体的には、

  • コードの理解・修正にかかる時間の増加
  • バグの発生率の上昇
  • 新機能追加の難易度上昇

などの形で現れます。この「負のスパイラル」は時間とともに加速し、最終的にはシステム全体の健全性を脅かすことになるのです。

クラフトは計画的なリファクタリングによってのみ解消できる

ウォード・カニンガムもマーティン・ファウラーも、「クラフトを解消する最も有効な手段はリファクタリングである」と指摘しています。

クラフトは放置していても、残念ながら自然に消えることはありません。それどころか、コードベースにおける変更コストの非線形的増大を通じて、時間とともにシステム全体の開発速度を低下させます。ファウラーが述べるように、これは「設計上の腐敗」として現れ、修正コストが逓増する前に手を打たなければ、後のリファクタリングは指数的に困難になってしまうからです。

そのため、XP(eXtreme Programming)の原則では、リファクタリングは「変更のための投資活動」とみなされ、機能開発と同列のタスクとしてスプリント内に組み込むことが推奨されています。重要なのは、クラフトが顕在化した箇所を単に修正するのではなく、体系的にリファクタリングを進めることです。

もちろん、クラフトの解消には必ずコストがかかります。全てのクラフトを悪と捉えて、躍起になって修正する必要はありません。リファクタリングに要する時間、テストの再実行、レビューといった工数は、短期的には機能開発の速度を低下させます。

一方で、クラフトを残したままメンテナンスを続けると、変更コストが継続的に増大し、最終的には累積コストが解消コストを上回ります。この二つのコストを比較し、解消の優先順位と実施タイミングを定めることが、合理的なクラフト管理の出発点となるのです。

なぜクラフトが溜まる? 欠けているのは「整えるフェーズ」

アジャイル開発やリーン開発では「速く出す」ことが重視されます。意思決定のサイクルを短くし、顧客からのフィードバックを迅速に得ることで、学習速度を高めるのが目的だからです。ただし、この「学習速度」と「実装速度」は混同されがちではないでしょうか。コードを速く書くことは、学習を早めるための手段であり、ゴールではないはずです。

スピードを優先するチームは、意思決定を「仮説」として捉えることがあります。つまり、完全な理解を待たずに仮説として設計・実装を進めるのです。これは合理的な選択ですが、その仮説が外れたまま長期間放置されると、コードベースに局所的な歪み——すなわちクラフト——が生じてしまいます。問題の本質は、スピードそのものではなく、「仮説を検証した後に修正する時間を確保しないこと」にあるのです。

多くのチームでは、技術的負債を記録せず、修正の優先順位を明示的に管理しないため、クラフトが静かに堆積していきます。コードの整合性を取り戻す機会はスプリント外に追いやられ、結果として「速く出す」ことが「修正できない構造」を作り出す……。これが、短期のスピードが長期の速度を奪う典型的なパターンです。

ウォード・カニンガムが述べたように、負債は返済を前提とする限り健全なものです。問題は、返済計画のない負債を繰り返し発生させることであり、それがクラフトの主要な供給源となります。スピード重視の開発を持続可能にするには、「どの負債をいつ返すか」を明文化し、リファクタリングを開発プロセスの一部としてスプリントに組み込む必要があるのです。

つまり、スピードと品質は対立しません。両者の調和は、「仮説としての実装」と「検証としてのリファクタリング」を一つの循環とみなせるかどうかにかかっています。クラフトが溜まるのは、速度の問題ではなく、サイクルの非対称性——「作るフェーズ」はあるが、「整えるフェーズ」が欠けている——ことによって生じます。従って、真にアジャイルなチームとは、速く作るだけでなく、速く修正できる構造を維持し続けるチームと言えるでしょう。

そして、その「戻せる構造」を制度的に支えるのが「意思決定の記録」です。どのような仮説のもとにどんな妥協を選択したのか、どのリスクを受け入れ、どの技術的負債を意図的に残したのかを明文化しておく必要があります。

この記録がないまま時間が経過すると、コード上の痕跡から意図を再構成することは困難になります。結果として、後任の開発者は「なぜそうなっているのか」を理解できず、同じ問題を再発させます。クラフトの多くは、このように技術的選択の文脈が失われた状態から生じるからです。従って、クラフトの抑制には、リファクタリングだけでなく、意思決定の透明性を保つ仕組みが不可欠です。

そのための具体的な手段が「ADR(Architectural Decision Record、アーキテクチャ決定記録」です。ADRは単なるドキュメントではなく、「現時点での最良の仮説」を記録するメタ構造です。

そこには、

  • 決定の背景
  • 選ばなかった代替案
  • 想定している寿命
  • 検証すべき条件

などが含まれます。これにより、将来の開発者は「この判断はどのような前提でなされたか」「どの時点で再評価すべきか」を再現的に理解できるわけです。

つまりADRとは、技術的負債を再構築可能な形で管理する装置です。負債そのものを悪とみなすのではなく、いつ・どのように返済するかをチームが合意できるようにするための「会計帳簿」のようなものです。これが存在すれば、「速く作る」ことは短絡的なスピードではなく、可逆的な実験としてのスピードに変わります。コードだけでなく意思決定の履歴までもがリファクタリング可能な構造になるのです。

従って、クラフトを溜めないために必要なのは、スピードを抑えることではありません。むしろ、速く動きながらも意思決定の痕跡を残す習慣を持つことです。ADRはそのための最小限の形式であり、継続的リファクタリングと並んで、技術的負債を「健全な仮説」として管理するための基盤と言えるでしょう。

scrapbox.io

【ケーススタディ】 技術的負債に伴って発生するクラフトの4分類と対処法

技術的負債以外の原因で発生するクラフトの抑制方法については別の記事に譲り、ここでは「技術的負債の選択によって生じるクラフト」にフォーカスします。なぜなら、負債は意思決定の履歴(前提・仮説・妥協)が付随しやすく、ADRを整合させて返済計画に落とし込みやすい対象だからです。

ここではそのメカニズムを分析し、クラフトの発生確率を低減するための方策を検討していきましょう。

設計時点の予測と、後になって判明した事実との乖離。これこそが技術的負債に伴って発生しやすい問題ですが、大きく以下の4つに分類できます。

  • 【1】混在:異なる概念が同じ場所に置かれる
  • 【2】分散:同じ概念が分割されて別々の場所に置かれる
  • 【3】冗長:同じ概念が重複してあちらこちらに置かれる
  • 【4】欠落:必要なのに実装されていない

【1】混在:異なる概念が同じ場所に置かれる

設計時には業務理解が足りず、本来分離すべき概念を「同じもの」として扱ってしまうことがあります。

ECサイトの有料会員プランで利用可能な機能を判定する例をもとに見ていきましょう。

会員プランには以下の3つがあり、それぞれ利用可能な機能が異なります。

  • フリープラン:基本的な購入機能のみ
  • スタンダードプラン:送料無料、5%割引
  • プレミアムプラン:送料無料、10%割引、優先配送、限定セール参加

このとき、契約プランによって機能の利用可否を判定するとどうなるでしょうか。プランやその特典は、会員獲得やリテンション向上を目的として頻繁に追加・変更されます。

例えば、プラン判定ロジックがControllerに直接書かれている場合を考えてみます。

@RestController
@RequestMapping("/api/orders")
public class OrderController {    
    @GetMapping("/sale-items")
    public ResponseEntity<List<Item>> getSaleItems(@AuthenticationPrincipal User user) {
        String plan = user.getMembershipPlan();
       
        // プレミアム会員のみ限定セール商品を表示
        if ("PREMIUM".equals(plan)) {
            return ResponseEntity.ok(itemRepository.findSaleItems());
        }
       
        return ResponseEntity.ok(Collections.emptyList());
    }
   
    @PostMapping("/express-shipping")
    public ResponseEntity<ShippingResponse> requestExpressShipping(
            @RequestBody ShippingRequest request,
            @AuthenticationPrincipal User user) {
       
        String plan = user.getMembershipPlan();
       
        // プレミアム会員のみ優先配送可能
        if (!"PREMIUM".equals(plan)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(new ShippingResponse("優先配送はプレミアム会員限定です"));
        }
       
        // 優先配送処理...
        return ResponseEntity.ok(new ShippingResponse("優先配送を受け付けました"));
    }
}

この実装では、ユーザーの契約プランを取得し、それに基づいて機能を制御しています。

プランが追加されたり、プランで使える特典が変わったりすると、これがクラフトになります。判定のif文をあちこち修正しなければなりません。また、契約プランで判定しているため、契約なしで有料機能が使えるキャンペーンを実施する際も、このif文の修正が大変になってしまいます。

【対処法】

これは契約プランと使える特典の制御を一体化して考えてしまっていることが原因です。次のように契約プランと適用される特典を切り離すことで、プランを追加したり内容を変更したときの影響範囲を小さくできます。

// 特典をEnumで定義
public enum Benefit {
    FREE_SHIPPING,
    DISCOUNT_5,
    DISCOUNT_10,
    EXPRESS_SHIPPING,
    SALE_ACCESS
}


// Controllerでの使用例
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final UserBenefitRepository benefitRepository;
   
    @GetMapping("/sale-items")
    public ResponseEntity<List<Item>> getSaleItems(@AuthenticationPrincipal User user) {
        Set<Benefit> benefits = benefitRepository.findByUser(user);
       
        if (benefits.contains(Benefit.SALE_ACCESS)) {
            return ResponseEntity.ok(itemRepository.findSaleItems());
        }
       
        return ResponseEntity.ok(Collections.emptyList());
    }
   
    @PostMapping("/express-shipping")
    public ResponseEntity<ShippingResponse> requestExpressShipping(
            @RequestBody ShippingRequest request,
            @AuthenticationPrincipal User user) {
       
        Set<Benefit> benefits = benefitRepository.findByUser(user);
       
        if (!benefits.contains(Benefit.EXPRESS_SHIPPING)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(new ShippingResponse("優先配送特典がありません"));
        }
       
        return ResponseEntity.ok(new ShippingResponse("優先配送を受け付けました"));
    }
}

業務理解が浅い段階では、こうした判断は難しく、「とりあえずリリースしてから考えよう」と技術的負債を選択しがちです。しかし、セールス都合での仕様変更は、他の機能に比べて頻繁に発生します。このように変動性の異なるものは、初めから切り離しておくことで、クラフトを最小限に抑えられるのです。

【2】分散:同じ概念が分割されて別々の場所に置かれる

分散のクラフトは技術的負債を選択しなくても発生する可能性があります。過剰なレイヤー定義などによって、責務の不明確な処理が作られることがあるからです。

よくあるのは、Web層とService層を分けているものの、各レイヤーで扱うデータが変わらない、または同じような型に詰め替えているだけというケースです。これでは再利用性はなく、結局Web層とService層のコンポーネントは密結合してしまいます。

具体的に見ていきましょう。ホテルの予約のエンドポイントで、次のようなリクエストを受け取るとします。

record ReservationRequest(
    @NotBlank(message = "ホテルIDは必須です")
    String hotelId,
   
    @NotBlank(message = "プランIDは必須です")
    String planId,
   
    @NotBlank(message = "部屋タイプIDは必須です")
    String roomTypeId,
   
    @NotNull(message = "チェックイン日は必須です")
    @Future(message = "チェックイン日は未来の日付である必要があります")
    Date checkInDate,
   
    @NotNull(message = "チェックアウト日は必須です")
    @Future(message = "チェックアウト日は未来の日付である必要があります")
    Date checkOutDate,
   
    @Min(value = 1, message = "大人の人数は1人以上である必要があります")
    @Max(value = 10, message = "大人の人数は10人以下である必要があります")
    Integer numberOfAdults,
   
    @Min(value = 0, message = "子供の人数は0人以上である必要があります")
    @Max(value = 10, message = "子供の人数は10人以下である必要があります")
    Integer numberOfChildren
) {}

このリクエストをもとに予約サービスを呼び出します。予約サービスはService層のコンポーネントなので、ReservationRequestはそのまま使わず、似たような構造のReservationDtoに詰め替えます。

record ReservationDto(
    String hotelId,
    String planId,
    String roomTypeId,
    Date checkInDate,
    Date checkOutDate,
    int numberOfAdults,
    int numberOfChildren
) {}


interface ReservationService {
    void registerReservation(ReservationDto dto);
}

この2つの型はほぼ同じ構造を持っていますが、バリデーションアノテーションの有無だけが異なります。Web層のReservationRequestにはバリデーションアノテーションがありますが、Service層のReservationDtoには存在しません。

Web層でバリデーションを行っているため一見問題ないように思えますが、予約サービスを他のエンドポイントや別のシステムから再利用する際に問題が発生します。ReservationDtoが満たすべき条件が分からないからです。Web層とService層でレイヤーを分けるなら、Serviceの呼び出し条件をReservationDtoの不変条件として表現しなければなりません。そうしなければ、Serviceは特定のエンドポイント専用になってしまいます。

【対処法】

予約に関するデータの処理の分散を避けるためには、業務としてValidであることを保証する型を作るのが有効です。

public class Reservation {
    private final String hotelId;
    private final String planId;
    private final String roomTypeId;
    private final DateRange stayPeriod;
    private final GuestCount guestCount;
   
    public Reservation(
            String hotelId,
            String planId,
            String roomTypeId,
            Date checkInDate,
            Date checkOutDate,
            int numberOfAdults,
            int numberOfChildren) {
       
        if (hotelId == null || hotelId.isBlank()) {
            throw new IllegalArgumentException("ホテルIDは必須です");
        }
        if (planId == null || planId.isBlank()) {
            throw new IllegalArgumentException("プランIDは必須です");
        }
        if (roomTypeId == null || roomTypeId.isBlank()) {
            throw new IllegalArgumentException("部屋タイプIDは必須です");
        }
        if (checkInDate == null || checkOutDate == null) {
            throw new IllegalArgumentException("チェックイン日とチェックアウト日は必須です");
        }
        if (checkInDate.after(checkOutDate)) {
            throw new IllegalArgumentException("チェックアウト日はチェックイン日より後である必要があります");
        }
        if (numberOfAdults < 1 || numberOfAdults > 10) {
            throw new IllegalArgumentException("大人の人数は1人以上10人以下である必要があります");
        }
        if (numberOfChildren < 0 || numberOfChildren > 10) {
            throw new IllegalArgumentException("子供の人数は0人以上10人以下である必要があります");
        }
       
        this.hotelId = hotelId;
        this.planId = planId;
        this.roomTypeId = roomTypeId;
        this.stayPeriod = new DateRange(checkInDate, checkOutDate);
        this.guestCount = new GuestCount(numberOfAdults, numberOfChildren);
    }
}

このReservationクラスは、コンストラクタで全ての不変条件をチェックし、インスタンスが生成された時点で業務として妥当な状態であることを保証します。Web層ではReservationRequestからReservationを生成し、Service層ではReservationを受け取ります。コードに重複はありますが、それぞれが異なる関心事を表現しています。Web層のバリデーションはユーザー体験のため、ドメイン層の不変条件はビジネスルールの保護のためです。

なお実装の工夫により、コードの重複も避けることも可能です。詳しくは私がまとめた『バリデーション解体新書』の記事をご参照ください。

scrapbox.io

【3】冗長:同じ概念が重複してあちらこちらに置かれる

冗長は「DRY原則(「同じことを繰り返し書かない」というソフトウェア設計の基本原則)」に違反するものであり、技術的負債とは無関係にクラフトとして現れることがあります。

しかし、ドメインへの理解が浅い段階でDRY原則にこだわり過ぎると、誤った抽象化をしてしまう危険があります。DRY原則の本質は「コードの重複を避けよ」ではなく「知識の重複を避けよ」です。十分な知識がなければ、この判断はできません。そのため、DRY原則を適用する前には、Rule of Threeを意識しましょう

Three strikes and you refactor(3回出るまで抽象化しない)

一方で、初めから考えておいた方が良いものもあります。多くのシステムには、他の機能に比べて明らかに追加・変更が頻繁に発生する「ホットスポット」が存在します。

例えばモバイルオーダーのシステムで、以下のようなキャンペーンを実施したい要求があるとしましょう。

  • 初回注文20%OFF(アプリ限定)
  • オフピーク100円引き(平日14:00-17:00)
  • アプリ決済5%OFF(ドリンクカテゴリ限定)

今後どういうキャンペーンがあるかも分からないので、キャンペーンごとにバラバラに実装することは、技術的負債を選択する観点からも理にかなっていると言えます。

しかし、キャンペーンが増えるにつれて、同じような割引計算ロジックがあちこちに散らばり、保守性が低下していきます。さらに、各割引は独立しているわけではなく、適用順序や併用条件(例:「初回割引とオフピーク割引は併用可能だが、アプリ決済割引との併用は不可」など)が存在するため、キャンペーン間の関係性も管理しなければなりません。

このような場合は、早い段階でキャンペーンを抽象化する仕組みと、割引の適用ルールを一元管理する設計を導入しておかないと、後からの追加は難しくなってしまいます。

【対処法】

// キャンペーンの抽象化
public interface Campaign {
    boolean isApplicable(Order order, OrderContext orderContext);
    BigDecimal calculateDiscount(Order order);
    default int getPriority() { return 1; }
}


// 初回注文キャンペーン
public class FirstOrderCampaign implements Campaign {
    private final BigDecimal discountRate = new BigDecimal("0.20");
   
    @Override
    public boolean isApplicable(Order order, OrderContext ctx) {
        return ctx.isFirstOrder() && ctx.isFromApp();
    }
   
    @Override
    public BigDecimal calculateDiscount(Order order) {
        return order.getTotalAmount().multiply(discountRate).setScale(0, RoundingMode.DOWN);
    }
}

// オフピーク割引
public class OffPeakCampaign implements Campaign {
    private final BigDecimal discountAmount = new BigDecimal("100");
   
    @Override
    public boolean isApplicable(Order order, OrderContext ctx) {
        int hour = order.orderedAt().getHour();
        DayOfWeek dayOfWeek = order.orderedAt().getDayOfWeek();
        return dayOfWeek != DayOfWeek.SATURDAY
            && dayOfWeek != DayOfWeek.SUNDAY
            && hour >= 14 && hour < 17;
    }
   
    @Override
    public BigDecimal calculateDiscount(Order order) {
        return discountAmount;
    }
}


// キャンペーン管理サービス
public class CampaignService {
    private final List<Campaign> campaigns;
   
    public CampaignService(List<Campaign> campaigns) {
        this.campaigns = campaigns.stream()
            .sorted(Comparator.comparing(Campaign::getPriority))
            .collect(Collectors.toList());
    }
   
    public BigDecimal applyDiscounts(Order order, OrderContext orderContext) {
        BigDecimal totalDiscount = BigDecimal.ZERO;
       
        // 併用条件は未実装; 必要があればここに実装する
        for (Campaign campaign : campaigns) {
            if (campaign.isApplicable(order, orderContext)) {
                BigDecimal discount = campaign.calculateDiscount(order);
                totalDiscount = totalDiscount.add(discount);
            }
        }
       
        return totalDiscount;
    }
}

これは「【1】混在」で述べたセールスの都合と通じるものがあります。キャンペーン機能は頻繁な変更が予想される領域であり、早期に抽象概念を作っておく価値が高いホットスポットです。

こういったホットスポットは、同種のOSSプロダクトやSaaSプロダクトの設計を知っておくと勘所がつかめるようになります。

例えば、今回のディスカウントの仕組みなら、ShopifyのDiscount APIのような柔軟な割引設定が参考になります。Shopifyでは、割引の種類(金額ベース、パーセンテージベース)、適用条件(商品カテゴリ、顧客セグメント、注文金額など)、併用ルールなどを統一的なインターフェースで管理できます。こうした既存の成功事例から学ぶことで、将来クラフトになりそうな領域を見極め、適切な抽象化のタイミングを判断できるようになるでしょう。

【4】欠落:本当は必要なのに実装されていない

技術的負債を選択する際、「今は必要ないので作らない」と判断するのは、実は非常に難しいものです。 例として、次のようなファイルアップロード機能を作るケースを考えてみましょう。

  • Excelファイルをアップロードし、内容をパースしてデータベース登録する。
  • 元のファイルも残しておき、ダウンロードできるようにする。

このとき、どこまで作り込む必要があるでしょうか? 例えば、以下のような実装判断が考えられます。

  • 同期的にアップロードするか? 非同期にするか?
  • アップロード中のユーザーへの進捗表示をするか?
  • エラーレコードをユーザーにどう知らせるか?
  • ファイルサイズの上限をチェックするか?
  • アップロードファイルを一時ファイルに書き出す設計にするか?(※ただし、処理中断時にゴミファイルが残るリスクの考慮が必要)
  • アンチウイルスのチェックをするか?

これらのうち、現時点で「おそらく不要」と判断して実装を省略したものが、後になって「実は必要だった」と判明するケース。これこそが「欠落」に該当します。技術的負債として「今は作らない」と決めたつもりでも、実際には「作るべきだった」機能が抜け落ちているのです。

【対処法】

非機能要求が明確に定義されていないと、欠落として現れやすくなります。性能要件、セキュリティ要件、可用性要件などが曖昧なまま開発を進めると、後になって「この規模のデータ量では処理が遅過ぎる」「このレベルのセキュリティ対策が必要だった」といった問題が発覚します。これはシステム運用継続の「ノックアウトファクター」にもなりえます。

開発するシステムのミッションクリティカル性にもよりますが、非機能要求に関わる実装は、決して後回しにして良いものではありません。なぜなら、非機能要求に関わる問題は後から対応しようとすると、システムアーキテクチャの根本的な見直しが必要になるケースが多いからです。

例えば、同期処理で作られたファイルアップロード機能を後から非同期化しようとすると、UI層からデータベース層まで広範囲な変更が必要になります。また、セキュリティ対策が不十分なまま本番稼働してしまうと、個人情報漏洩などの重大なインシデントにつながるリスクがあります。

技術的負債として「後で対応する」という選択は、機能要求に対しては有効です。しかし、非機能要求に対しては、往々にして取り返しのつかない設計上の欠陥を生み出してしまうのです。

クラフトの性質を理解して継続的な改善を

ここまで技術的負債に伴うクラフトの分類と、その抑止可能性について見てきました。当然ながらクラフトをゼロにすることはできません。

しかし、本記事で示したように、技術的負債を選択する際の意思決定において未来予測を完全に捨てる必要はありません。むしろ、過去の経験や類似システムの知見、チームの技術的強み、そしてドメインの特性を考慮に入れることで、より賢明な判断ができるはずです。

例えば、キャンペーン機能のように頻繁な変更が予想される領域では早期の抽象化を、一方で一時的な機能では最小限の実装を選択するといった、メリハリのある意思決定が可能になります。

完璧なコードベースを目指すのではなく、技術的負債とクラフトの性質を理解し、継続的な改善を通じて、ビジネス価値を提供し続けながらシステムの品質を維持していく。それが現実的で持続可能なアプローチと言えるでしょう。

合わせて読みたい注目記事

編集・制作:はてな編集部

*1:米国のプログラマー。「Wiki」の創案・開発者として知られる

*2:米国で活動するソフトウェアエンジニア。『リファクタリング』の著者

川島義隆
川島義隆(かわしま・よしたか) X: @kawasima
流しのアーキテクト。世の中の設計論を咀嚼して、各現場に適応させることを生業としています。これまで考えたこと・発表したことはhttps://scrapbox.io/kawasimaにまとめています。