はじめに

2026年6月に約3週間、CA Tech Jobに参加しました、早稲田大学基幹理工学部情報理工学科3年の小林加弦(@kobakaito)と申します。大学ではコンピュータサイエンスを専攻し、アルバイト・業務委託でバックエンド・インフラ領域を中心とした開発業務に従事しています。

今回のインターンシップでは、課金システムやプラットフォーム基盤といったABEMAのコア機能の開発を担うMonetize Platformチームで、サブスクリプションページの運用改善に取り組みました。本記事ではその取り組みをご紹介します。

 

取り組みの背景

ABEMAには、課金内容をユーザーに案内する「サブスクリプションページ」があります。

サブスクリプションページは、「セクション」と呼ばれる単位の組み合わせで構成されており、管理画面から入稿されています。

同じ内容のセクションを複数のページで使い回したいケースは多くあります。しかしこれまでは、同じ内容であってもページごとにセクションを個別に設定する必要がありました。そのため、ページ数が増えるにつれて、運用での入稿負荷が高くなることが課題となっていました。

そこで今回の取り組みでは、セクションをモジュール化して複数ページ間で再利用できるようにすることで、入稿負荷を改善しました。

設計の進め方

要件の整理 → DB設計 → API設計 → QA戦略の順で設計を進めました。

今回の機能は課金に直結するため、リリース後も安定して運用され続ける必要があります。一方で、就業型インターンという形式上、私の在籍期間は3週間に限られます。そこで、退職後に機能をブラックボックス化させないことを重視しました。

具体的には design docを執筆し、採用した設計だけでなく、その背景となるコンテキストや検討過程も含めてチームに共有しました。検討過程の記述では、採用した設計と採用しなかった設計の双方を pros/consベースで比較し、この機能のコンテキストに詳しくない方でも設計の合理性を追えるようにしました。採用した設計についてもメリットだけでなくデメリットを明記することで、将来の技術的負債のリスクをチームで共有できるという利点もあります。

 

設計

機能要件

以下の2つの機能を実装することにしました。

  • module sectionのCRUD API(以降、モジュール化したセクションを「module section」と呼びます)
  • Subscription Pageに関連する既存APIへのmodule section参照の導入
    • 稼働中の課金導線に手を入れる変更になります

module sectionのライフサイクルのイメージは以下の通りです。

管理者がmodule sectionを作成し、Subscription Pageから参照できるようになります。module sectionに更新・削除操作がかかると、そのmodule sectionを参照するSubscription Pageもその変更に追従します。

非機能要件

以下のように非機能要件を整理しました。

  • 後方互換性
    • 既存機能に破壊的変更を加えない。課金に直結するため、不具合が生じた際のビジネスインパクトが大きい
  • パフォーマンス
    • module sectionを管理するテーブルへのアクセスで、N+1問題などの性能劣化を起こさない
      • 例えば、1つのSubscription Pageに複数のmodule sectionが紐づくとき、module sectionごとに fetchクエリを発行するとN+1問題が発生します。これはIN句を用いたbulk fetchで解決できます
  • データ整合性
    • バリデーション・参照整合性の担保などをし、データの不整合を起こさない
  • 認可
    • module sectionのCRUD APIに、一般ユーザーからのアクセス経路を作らない

DB設計

以下のようにmodule sectionを格納するDB(PostgreSQL)の設計を行いました。

ここでは、workloadの分析と、index選定についてご紹介します。

workload の分析

module sectionを管理するテーブルへのアクセスは、圧倒的にread heavyとなります。理由としては、読み取り操作は、管理画面経由だけでなく、サブスクリプションページを閲覧する一般ユーザーのリクエスト経由によっても発生し、そのアクセス数は非常に多いためです。一方で、書き込みは、管理画面経由での運用作業に限られるため低頻度となります。

以上から、読み取り性能を優先する方針としました。

 

index選定

indexの選定においては、「現行の検索要件を満たす最小構成は何か」を起点に判断しました。

今回の検索の要件としては、等価検索・前方一致検索が必要でした。そのため、以下のようにindexを貼りました。

  • 検索で頻繁に使われるカラムにはB-tree indexを作成
  • jsonb 型のカラムについては、B-tree indexでは検索を扱えないためGIN indexを作成
    • GIN indexは書き込みコストが比較的大きいものの、前述の通り書き込みは低頻度の運用作業に限られるため、読み取り性能の改善というメリットが上回ると判断しました。

また、将来的な要件として、曖昧検索・全文検索が追加される可能性がありますが、要件のない段階で他にindexを追加しても、index sizeや書き込みコストをいたずらに増やすことになると判断し、今回は追加せず、将来要件が生じた時点で追加する方針としました。

採用しなかった設計

design docでは、採用した設計と併せて、採用しなかった設計とその理由も記述しました。代表的なものを1つご紹介します。

クラステーブル継承によるDB設計

module sectionは、セクションの種類によって保持するコンテンツの構造が大きく異なります。この多態性をDBでどう表現するかが1つの焦点でした。

クラステーブル継承は、共通の属性を親テーブルに持たせ、種類ごとの固有フィールドを子テーブルとして切り出す設計パターンです。

しかし今回のケースでは、コストとメリットの両面から採用を見送りました。

  • スキーマとして維持するコストが大きい
    • セクションの種類ごとに、その中身の構造が大きく異なり、それぞれを厳密に正規化した子テーブルとして表現しなければならない。さらに、セクションの種類の追加や、中身の構造の変更のたびにテーブルの新設やスキーマ変更が必要になる
  • 正規化で得られるメリットが小さい
    • スキーマで構造を縛る主なメリットとしては、セクションの中身に対する検索のしやすさや、整合性の担保が挙げられる。しかし、JSONで保持した場合でも、検索はGIN indexによって高速に実現でき、整合性の担保はアプリケーション層でカバーできる。

このため、種類ごとに異なるコンテンツの構造は、スキーマで縛らず、JSONとして柔軟に保持する設計としました。

QA戦略

最も重視したのは「既存機能に破壊的変更を加えないこと」です。実装範囲は課金システムであり、かつ一般ユーザーが利用するエンドポイントを含み、不具合が生じた際のユーザー影響が大きいためです。

そこで、新機能の実装に着手する前に以下を徹底し、既存機能への破壊的変更をCIで検知できる体制を整えました。

  • 既存のテストケースは一つも消さない
  • 既存のテストケースが十分かを検討し、必要に応じて拡充する

そのうえで、新機能が正しく動作することを自動テストで担保しました。

 

実装

以下の順で実装を進めました。

  1. PostgreSQLへのschema migration
  2. module sectionのCRUD APIの実装
  3. Subscription Pageに関連する既存APIへのmodule section参照の導入

 

設計量・実装量に対して、期限(インターンシップ終了日)はタイトでした。手戻りを減らし、PRがマージされるまでのリードタイムを短くするため、以下の点を工夫しました。

PRの出し方・粒度を、実装に入る前にチームとすり合わせる

  • PRの説明欄やセルフレビューコメントを充実させ、レビュアーが少ない認知負荷でレビューできるようにする
  • 相談事項はできるだけ早い段階で共有する
  • 自分が進んでいる方向性を定期的に共有する

その結果、大きな手戻りなく、PRのリードタイムも短く保ちながら開発を進めることができました。

 

リリース・動作確認

インターンシップ期間内に無事リリースを完了できました。

既存の Subscription Page系APIに加えた変更については、今回の差分に対して feature flagを設定しました。feature flagが有効になるのは動作検証用のリソースに限定し、それ以外に対しては既存の挙動がそのまま維持されるようにしています。これにより、既存機能に影響を与えない状態で安全にリリース・検証を進められます。

この状態で開発環境にリリースし、直接APIを呼び出す形で動作確認を行い、以下の2点を確認しました。

  • module section の一連のライフサイクル(作成→Subscription Pageからの参照→更新→削除)で、期待通りのレスポンスが返り、DBにも正しくデータが格納されること
  • 既存機能に破壊的変更(既存フィールドの欠損など)が生じていないこと

 

 

インターンシップを通して学んだこと・感じたこと

ミッションクリティカルな開発の経験

大規模サービスの課金基盤という高い信頼性の求められるドメインで、新たな機能だけでなく、ビジネスインパクトの大きい既存機能を改修する開発を経験できたのは、非常に学びのある経験でした。

今回の開発では、不具合が膨大なインパクトを引き起こしうるからこそ、後方互換性を意識した設計、パフォーマンスへの考慮、既存機能を守るQA戦略、feature flagを用いた安全なリリースといった、「壊さないための技術」に力を注ぎました。日本を代表する大規模なサービスを舞台にそれを経験できたことは、深い学びになったと同時に、自信につながりました。

ABEMAの圧倒的な技術水準の高さ

今回心に残ったのは、ABEMAの圧倒的な技術水準の高さです。

ABEMAは巨大マイクロサービス群、膨大なトラフィック、豊富な機能、莫大なユーザー影響とビジネスインパクト、動画配信という特殊なドメインなど、エンジニアとして非常に難しい課題が多くあります。

メンバーの多くが若手ながらも、一人一人がオーナーシップを持ってプロダクトに向き合い、最高レベルの信頼性と開発効率を実現する姿に、非常に感銘を受けました。

また、トレーナーのユシンさんからは、豊富な業務経験と理論的な知識双方をベースとして高いパフォーマンスを出す姿勢を学びました。私のこれまでを振り返ると、難しい課題に挑戦し実践を通して学ぶ、いわばトップダウンの学習が中心でした。しかしそれだけでなく、ファンダメンタルな知識を積み上げるボトムアップの学習を併用して力を高めていくことの重要性を強く感じました。

 

 

最後に

本インターンシップでは、大規模サービスを舞台に、非常に貴重な経験をさせていただきました。この経験を活かし、今後も研鑽を重ね、様々なことにチャレンジしていければと思います。

最後になりますが、トレーナーのユシンさん、メンターの大真さん、Monetize Platformチームの皆さん、担当人事の宝田さんをはじめ、ご尽力いただいた社員の皆さんに感謝を申し上げます。

本当にありがとうございました!