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

こんにちは。新規事業部門でエンジニアをやっている重本です。 2025年5月にReproへ入社し、この記事を書いている今でちょうど1年が経とうとしています。 転職して環境が変わったのはもちろんですが、第一子が生まれたりと、個人的にもなかなか変化の大きい1年でした。

そんな中で、一番大きな変化だったと感じているのは、opus4.6 の登場をきっかけに、ほとんど手でコードを書かなくなったことです。(子どもじゃないのか)

最初は「自分の実装力が落ちるのではないか」といった抵抗もありましたが、今ではもうAIなしでの開発は考えられない、というところまで来ています。

とはいえ、AIの使い方自体はまだ試行錯誤の段階です。 ただ、新システムの開発に取り入れてみたところ、全体としてはかなり高い生産性を実感することができました。

その過程で採用したアーキテクチャも、AIと組み合わせることでうまく機能した場面が多くありました。 そこで本記事では、その構成と設計意図を整理して紹介してみようと思います。

はじめに

今回のシステムを設計するにあたり、いくつか明確な要件がありました。

1. 複雑なドメインロジックとその変更容易性 管理画面側の業務ロジックは分岐や条件が多く、かつ仕様変更も頻繁に発生するため、柔軟に修正できる構造が求められました。 2. 大量データの高速処理 数万〜数十万件規模のレコードを対象としたバッチ処理や集計処理があり、効率よく安定して処理できる実行基盤が必要でした。 3. 外部向けAPIのスループット 外部に公開するAPIでは、一定の負荷に耐えられるスループットと応答性能が求められました。

これらの要件はそれぞれ性質が異なり、単一の技術スタックですべてを最適化するのは難しいと判断しました。

アーキテクチャ概要

構成

.
├── db/                                  # DB スキーマ管理 (Single Source of Truth)
│   ├── schema.sql                       # マスタースキーマ (これだけ手動編集)
│   ├── migrations/                      # goose マイグレーション (自動生成)
│
├── go/                                  # Go 実装
│   ├── shared/                          # Go アプリケーション共通
│   │   ├── sqlc.yaml                    # sqlc 設定
│   │   ├── query.sql                    # sqlc 用クエリ定義 (手動編集)
│   │   └── db/                          # sqlc 生成コード (自動生成)
│   │
│   ├── api-server/                      # 外部連携用 REST API
│   │
│   └── batch-jobs/                      # バッチ処理
│
└── typescript/                          # TypeScript 実装
    ├── admin-api/                       # 管理コンソール API (NestJS)
    │   ├── src/
    │   │   └── domain/                  # ドメインモジュール (GraphQL)
    │   └── prisma/
    │       └── schema.prisma            # Prisma スキーマ (db:pull で自動生成)
    │
    └── admin-ui/                        # 管理コンソール UI (React)

このアーキテクチャには大きく2つの特徴があります。 1つは、責務ごとに Go と TypeScript を使い分けたハイブリッド構成であること。 もう1つは、それを支える基盤として、db/schema.sqlを Single Source of Truth(唯一の正しい情報源) として扱っている点です。

今回は前者である Go と TypeScript の使い分けについて整理していきます。

なぜ Go と TypeScript を使い分けたのか

基本的な考え方はシンプルです。

変更が多く複雑な領域は TypeScript、
CPU負荷や並列実行を伴う処理は Go

という役割分担にしています。

api-server(Go)

外部向け API は、瞬間的にリクエストが集中する可能性があります。 この部分を Go で実装することで、

  • 高いスループット
  • 並列処理のしやすさ
  • CPU スケールによる拡張性

を確保しています。 また、管理画面とはプロセスを分けているため、
外部負荷が管理画面に影響しない構成になっています。

batch-jobs(Go)

バッチ処理は、

  • 大量データを扱う
  • 並列処理したい場面がある
  • 1件ごとの処理は比較的シンプルで、繰り返し構造になりやすい

という特徴があります。 Go の goroutine は、このようなワークロードと非常に相性が良く、
シンプルなコードで並列処理を書けるのが大きな利点です。

admin-api(TypeScript / NestJS)

管理画面側は性質がまったく異なります。

  • ドメインロジックが複雑
  • 仕様変更が頻繁に入る
  • 表現力が重要

この領域では、変更しやすさと書きやすさが重要になります。 TypeScript + Prisma を使うことで、

  • 柔軟にロジックを書ける
  • データアクセスの実装コストを下げられる

というメリットがあります。

まとめ(前編)

ここまでで紹介した構成は、単に「Go と TypeScript を併用している」という話ではなく、

  • 各言語の責務を明確に分ける
  • それぞれの特性に応じて適材適所で使い分ける

といった設計の組み合わせによって成り立っています。

このように役割を分離することで、

  • 変更しやすさ・複雑なドメインの扱いやすさ(TypeScript)
  • 実行性能・並列性(Go)

といったトレードオフをうまく両立できる構成になっています。

一方で、この構成を成立させているもう一つの重要な要素が、 db/schema.sqlを Single Source of Truth として扱う設計です。

次回(後編)では、

  • なぜ DB スキーマを Single Source of Truth にしたのか
  • この構成が AI と相性がいいと感じる理由

といった点について整理していきます。