汎用性抜群!DBスキーマを中心に据えた Go × TypeScript ハイブリッド構成の紹介 - (後編)

前編では、このアーキテクチャの概要と、Go と TypeScriptの使い分けについて紹介しました。 後編では、なぜ DB スキーマを Single Source of Truth にしたのかという設計判断の背景と、
この構成が AI と相性が良いと感じた理由について整理していきます。

なぜ DB スキーマを Single Source of Truth にしたのか

1. 生成元を一元化し、管理をシンプルにしたかった

Prisma と sqlc を併用する構成の場合、何も考えずに設計すると、スキーマの管理が分散しがちです。 例えば、以下のような流れです。

1. Prisma schema を更新
2. migration を更新
3. sqlc 用の schema も追従
4. Go 側の型生成を更新
5. 差分を人間が確認

一見問題なく運用できそうに見えますが、変更が積み重なると徐々に破綻していきます。

  • Prisma 側で追加したカラムは sqlc 側にも反映されているか?
  • Go 側で追加した制約は Prisma 側に入っているか?
  • そもそもどちらが「正」なのか?

このような状態になると、スキーマの整合性を人間の記憶に頼ることになり、認知負荷が一気に上がります。 そこで私たちは、永続化されるデータ構造については DB スキーマを基準にするという方針を取りました。

db/schema.sql を編集(これだけが手動作業)
    ↓
DB に適用
    ↓
Prisma / sqlc を generate
    ↓
すべての定義が同期される

この構成にすることで、

schema.sql を見れば、DB の構造はすべて分かる

という状態を維持できます。

実際の運用では、スキーマ変更は次のような手順で行っています。

スキーマ変更手順イメージ

  1. db/schema.sql を編集(これだけが手動作業)
  2. ローカル DB に適用 & マイグレーション生成
# ローカル DB に適用
psqldef < schema.sql
# マイグレーション自動生成
goose up
psqldef --dry-run < schema.sql
  1. 各アプリのスキーマ同期 make apply-db-schema-to-each-schema

    [!NOTE] apply-db-schema-to-each-schema は内部で以下を実行します

    • admin-api: prisma db pull → DB からスキーマを introspection
    • api-server / batch-jobs: sqlc generate → schema.sql + query.sql から型安全なコードを生成

2. DB の機能をフルに使いたかった

今回のシステムでは、バッチ処理で数万件規模のデータを扱う必要がありました。
そのため、パフォーマンス面での最適化は避けて通れません。

具体的には、

  • partial index
  • window functions
  • JSONB 操作

といった、PostgreSQL の機能を積極的に使いたいという要求がありました。 一方で、Prisma のような ORM は開発体験は非常に良いものの、複雑な集計や最適化が必要なケースでは、raw SQL を使う場面が増えるのも事実です。 それであれば最初から、

SQL の表現力を前提にした設計にしておいた方が、自由度が高い

と判断しました。

3. アプリケーションではなく「データ」を中心に据えたかった

Prisma を中心にすると、どうしても TypeScript アプリケーションが中心で、その下に DB があるという構造になります。 一方で今回採用したのは、逆の発想です。

DB schema(中心)
    ↓
┌─────────┬─────────┬─────────┐
↓         ↓         ↓         ↓
Prisma   sqlc     将来の   別の実装
(TS)     (Go)     コンポーネント

つまり、

データを中心にして、その周りにアプリケーションが存在する

という世界観です。 この形にしておくと、

  • 新しい言語で実装を追加する
  • ワーカーを増やす
  • 別サービスから参照する

といった拡張がしやすくなります。

なぜこの構成が AI と相性が良かったのか

この構成は、意図して設計した部分もありますが、実際に使ってみて「結果的にAIとかなり相性が良い」という感触がありました。 いくつか理由はあるのですが、大きくは次の2つです。

1. 責務の分離が明確

この構成では、各コンポーネントの責務をかなり明確に分けています。

  • 外部 API → Go
  • バッチ処理 → Go
  • 管理画面 API → TypeScript
  • UI → TypeScript

これによって、「この処理はどこに書くべきか」という判断がシンプルになります。 AI にコードを書かせる場合、この「迷わなさ」はかなり重要で、 実際にファイル配置のミスはほとんど起きませんでした。

2. schema.sql を中心とした一貫した構造

もう一つの大きな要因が、db/schema.sql を Single Source of Truth として扱っている点です。

AI がシステム全体のデータ構造を正確に理解するには、 情報が一箇所に集約されており、ブレがないことが重要です。

この構成では、db/schema.sql を読み込むだけで、 システム全体のデータ構造を一貫して把握できます。

さらに、

  • 手動で編集するファイル(schema.sql / handlers / domain)
  • 自動生成されるファイル(sqlc / Prisma)

といった境界が明確に分かれています。

そのため、「どこを編集すべきか」「どこは触ってはいけないか」がAIにも伝わりやすく、 生成コードを誤って編集してしまうといったミスも起きにくくなります。

実際には、生成手順をドキュメント(例:CLAUDE.md)にまとめておくだけで、 安定した運用が可能になりました。

まとめ

このアーキテクチャの本質は、次の3点に集約されます。

  • 役割ごとに技術を分ける(Go / TypeScript)
  • データを中心に据える(schema.sql)
  • トレードオフを明確に受け入れる

すべてのプロジェクトに適した構成ではありませんが、
条件が揃えば、

開発速度・実行性能・拡張性

をバランスよく実現できる構成になります。

おわりに

今回の構成を通して、特に重要だと感じたのは、

構造がシンプルで、依存関係が明確で、生成元が一意であること

この3つが揃っていることでした。
これらを満たすことで、人にもAIにも優しい設計につながっていました。

AIの進化によって、将来的には人の手をほとんど介さずに開発が進むようになるかもしれません。
それでも、何をどう設計するか、どこに責務を置くかといった判断は、引き続き人が担うべき重要な役割だと感じています。

これからも、そうした「人が考えるべき部分」に向き合いながら、より良いシステムを作り続けていきたいと思います。