なぜデータモデルがATSの「設計思想」を決めるのか
ATSを評価・統合・構築しようとするエンジニアが最初にぶつかる壁がデータモデルの違いです。 APIのエンドポイント名や認証方式の差異は表層的な問題ですが、データモデルの設計思想が異なると、 同じ「候補者管理」という機能でも、エンティティの命名・リレーション・APIのレスポンス構造が根本から変わります。
具体的に言うと、GreenhouseとLeverは「1人の候補者が複数の求人に応募する」という同じ現実をまったく異なる方法でモデル化しています。 この設計差異が、システム統合時のインピーダンスミスマッチ(impedance mismatch)の最大の原因になります。 HRIS連携や自社システムへのデータ取り込みを設計するエンジニアがこの違いを知らずに実装を始めると、 後から大規模なデータ変換レイヤーが必要になります。
本章では、ATSのコアドメインモデルを解剖し、主要設計思想の違い・遷移設計・スコアカード構造・マルチテナント設計の4テーマを扱います。 採用担当者がATSを使う「ユーザー視点」ではなく、ATSを設計・構築・統合する「エンジニア視点」でデータ構造を理解することがこの章の目的です。
ATSの5コアエンティティ
ATSのドメインモデルは、製品によって名称は異なりますが、本質的に5つのコアエンティティで構成されています。 まずこの5エンティティとその関係を押さえることが、あらゆるATSを理解する出発点になります。
erDiagram
JOB ||--o{ APPLICATION : "1:N"
CANDIDATE ||--o{ APPLICATION : "1:N"
JOB ||--o{ STAGE : "1:N"
APPLICATION }o--|| STAGE : "current_stage"
APPLICATION ||--o{ SCORECARD : "1:N"
SCORECARD }o--|| USER : "interviewer"
JOB {
uuid id
string title
enum status
string department
uuid hiring_manager_id
timestamp created_at
}
CANDIDATE {
uuid id
string name
string email
string phone
string source
timestamp created_at
}
APPLICATION {
uuid id
uuid candidate_id
uuid job_id
enum status
uuid current_stage_id
timestamp applied_at
}
STAGE {
uuid id
uuid job_id
string name
int order
}
SCORECARD {
uuid id
uuid application_id
uuid interview_id
uuid interviewer_id
enum overall_recommendation
timestamp submitted_at
}各エンティティの主要フィールドと役割を整理します。
Job(求人)
採用ポジションを表すマスターエンティティ。statusはopen / closed / draft / on_holdといった値をとります。hiring_manager_idはHiring Manager(採用責任者)へのFKで、 スコアカードの承認フローや通知設計に影響します。
Candidate(人物)
人物の永続的なIDを持つエンティティ。メールアドレスが事実上の自然キーになるケースが多いですが、 同一人物が異なるメールで複数回応募する現実があるため、重複検出(deduplication)ロジックが別途必要です。sourceはLinkedIn / リファラル / 直接応募などの流入経路を記録します。
Application(応募)
「1人のCandidateが1つのJobに応募した事実」を表す中間エンティティ(ジャンクションテーブル的な役割)。statusは終端状態機械(後述)、current_stage_idはパイプライン上の現在位置を示します。
Stage(選考ステップ)
「書類選考 → 電話スクリーニング → 一次面接 → 最終面接 → 内定」といったパイプラインの各ステップ。orderフィールドで表示順を管理します。 JobごとにStageをカスタマイズできるのがATSの基本機能です。
Scorecard(評価記録)
面接官(interviewer)が特定の面接について記録する評価シート。overall_recommendationはstrong_yes / yes / no / strong_noなどの値をとります。 属性ごとの評価点と重みを持つattributes配列が本体で、後述します。
Greenhouse型 vs Lever型 — 設計思想の分岐
5コアエンティティは共通ですが、「Candidate(人)とApplication(応募)の関係をどう設計するか」が ATSの最大の設計分岐点です。 グローバル市場の代表製品であるGreenhouseとLeverは、この点で根本的に異なる設計思想を持ちます。
Greenhouse型: Candidate + Application 分離
- Candidateが人物の永続IDを持つ
- 同一人物が複数Jobに応募 = 複数のApplicationを持つ
- ApplicationはCandidate_id + Job_idの複合FK
- 強み: シンプルで実装しやすい。REST APIが直感的
- 弱み: 1人の全応募履歴の横断分析には集計クエリが必要
GET /v1/candidates/:id
GET /v1/applications?candidate_id=:id
Lever型: Contact + Opportunity 統合
- Contactが人物の永続IDを持つ(Candidateと同義)
- Opportunity = 1人と1Jobの接点(ApplicationをCRM的に拡張)
- 1つのContactが複数のOpportunityを持てる
- 強み: 候補者の全タッチポイントをCRM的に管理できる
- 弱み: データモデルが複雑でAPI統合コストが高い
GET /v1/contacts/:id
GET /v1/opportunities?contact_id=:id
この設計差異は、API統合の実装コストに直接影響します。 Greenhouse型では「候補者の一覧」と「応募の一覧」が独立したエンドポイントで取得でき、データ変換が少なくて済みます。 Lever型はCRM指向のため、採用パイプライン管理の柔軟性は高いものの、 シンプルなHRIS連携では「OpportunityをApplicationとして扱うマッピング」が必要になります。
Stage/Status 遷移設計
ATSの設計で最も誤解されやすいのがStatus(ステータス)とStage(ステージ)の役割の違いです。 両者は明確に異なる概念です。
| 概念 | 役割 | 値の例 | 設計パターン |
|---|---|---|---|
| Status | Applicationの終端状態機械(State Machine) | active / rejected / hired | 有限状態機械(FSM) |
| Stage | パイプライン上の現在位置(どのステップにいるか) | Screening / Interview / Offer | Jobごとにカスタム可能な順序付きリスト |
Statusの状態機械設計
stateDiagram-v2
[*] --> active : 応募受付(applied)
active --> rejected : reject_candidate
active --> hired : hire_candidate
rejected --> active : unreject_candidate(復活)
hired --> active : unhire_candidate
hired --> [*]
note right of rejected
復活(unreject)を
許容するかは設計判断
end noteunreject(復活)の設計は実装上の重要な判断点です。 「一度落とした候補者を別のポジションで再検討する」という現実のユースケースをどう扱うか。 Statusをそのまま active に戻すと、 「なぜ一度落とされたか」という履歴が消えてしまいます。 このためAudit Trailの設計が不可欠です。
Stageパイプラインの設計
Stageはパイプライン上の位置を表し、Jobごとにカスタマイズできます。典型的なパイプラインは以下の通りです。
Audit Trail — 遷移ログの設計
Stageの変更ログ(Audit Trail)はKPI計算の基礎データです。stage_changeイベントには必ずタイムスタンプを記録します。 第4章で解説した「Time to Hire」は、最初のstage_changeタイムスタンプからhiredステータスになるまでの日数差で計算します。
-- Audit Trailテーブルの設計例
CREATE TABLE application_stage_changes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
application_id UUID NOT NULL REFERENCES applications(id),
from_stage_id UUID REFERENCES stages(id), -- NULL = 最初の応募
to_stage_id UUID NOT NULL REFERENCES stages(id),
changed_by UUID NOT NULL REFERENCES users(id),
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
reason TEXT -- rejectの理由など
);
-- Time to Hire の計算例
SELECT
a.id,
MIN(asc.changed_at) AS applied_at,
MAX(CASE WHEN a.status = 'hired' THEN asc.changed_at END) AS hired_at,
EXTRACT(DAY FROM
MAX(CASE WHEN a.status = 'hired' THEN asc.changed_at END)
- MIN(asc.changed_at)
) AS time_to_hire_days
FROM applications a
JOIN application_stage_changes asc ON asc.application_id = a.id
GROUP BY a.id;スコアカードのデータ構造
スコアカードはATSの「評価の核心」であり、採用の質(Quality of Hire)を後から分析するための最重要データです。 完全なデータ構造は以下のようになります。
{
"id": "scorecard_xxx",
"application_id": "app_xxx",
"interview_id": "interview_xxx",
"interviewer_id": "user_xxx",
"submitted_at": "2026-06-16T14:00:00Z",
"overall_recommendation": "yes",
"attributes": [
{
"name": "問題解決能力",
"rating": 4,
"weight": 0.25,
"note": "具体的なアーキテクチャ設計の事例を説明できた"
},
{
"name": "コミュニケーション",
"rating": 3,
"weight": 0.20,
"note": "技術的な説明は明瞭だが抽象化が不足"
},
{
"name": "技術的専門性",
"rating": 4,
"weight": 0.30,
"note": "PostgreSQLのクエリ最適化について深い知識を示した"
},
{
"name": "チームワーク・協調性",
"rating": 3,
"weight": 0.15,
"note": "過去のチーム経験の具体例が少なかった"
},
{
"name": "学習への意欲",
"rating": 5,
"weight": 0.10,
"note": "最新技術への興味と自己学習の習慣が明確"
}
],
"overall_score": 3.75
}overall_scoreは 各属性のrating(評価)と weight(重み)の積の合計で計算されます。 上の例では 4×0.25 + 3×0.20 + 4×0.30 + 3×0.15 + 5×0.10 = 3.75 です。
評価軸設計のガイドライン
| ガイドライン | 推奨 | 理由 |
|---|---|---|
| 評価軸の数 | 5〜8項目 | 多すぎると面接官の認知負荷が増し、評価精度が落ちる |
| 評価基準の記述 | 行動指標(BARS) | 「優秀」「普通」では面接官間でばらつく。具体的な行動例で定義する |
| 重み付け | 合計1.0になるよう設定 | ポジションごとに重要度が異なる軸の重みを変える |
| スケール | 5段階評価 | 4段階は中央値がなく強制選択になりすぎる。7段階は識別が難しい |
| 提出タイミング | 面接後24時間以内 | 記憶の新鮮さと、他の面接官の意見による汚染を防ぐ |
マルチテナントSaaS設計の4パターン
ATSはSaaSとして多数の企業(テナント)に同時にサービスを提供します。 「テナントのデータをどう分離するか」はセキュリティ・コスト・スケーラビリティのトレードオフを決定する設計の核心です。 代表的な4パターンを整理します。
| パターン | 分離方法 | コスト | セキュリティ | 適するケース |
|---|---|---|---|---|
| Pool | 共有テーブル + tenant_id カラム + RLS | 最安 | RLSミスで漏洩リスク | SMB・スタートアップ向け大規模展開 |
| Bridge | テナント別PostgreSQLスキーマ(search_path切り替え) | 中 | スキーマ分離で安全 | 中規模企業・規制業種 |
| Silo | テナント別独立DB(別インスタンス) | 最高 | 最強(DB完全分離) | 大企業・金融・医療・公共機関 |
| Hybrid | SMBはPool、エンタープライズはSilo | 中〜高 | テナント規模に応じた最適化 | 現実解: GreenhouseもLeverもこのパターン |
大多数のSaaS ATSはハイブリッド型を採用しています。 テナント数が数百〜数千社のSMB層はPoolで効率的に運用し、 年間契約額が数千万〜数億円になるエンタープライズ顧客にはSiloを提供してSLAを満たします。 PostgreSQLのRLSを使ったPool実装が最もコスト効率が高く、スタートアップが最初に採用すべきパターンです。
-- テナント分離のRLS設定例(PostgreSQL)
ALTER TABLE applications ENABLE ROW LEVEL SECURITY;
-- アプリケーション接続時にテナントIDをセッション変数にセット
-- SET LOCAL app.current_tenant = 'tenant-uuid-xxx';
CREATE POLICY tenant_isolation ON applications
USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- 管理者ロールはRLSをバイパス
CREATE POLICY admin_bypass ON applications
TO app_admin
USING (TRUE);
-- インデックス: RLSのパフォーマンス確保
CREATE INDEX idx_applications_tenant_id ON applications(tenant_id);
CREATE INDEX idx_applications_tenant_job ON applications(tenant_id, job_id);エンジニア視点のコラム: ATSのデータモデルはなぜ難しいか
ATSのデータモデル設計が難しい根本的な理由は、「同一人物が複数の文脈で存在する」という現実をモデル化しなければならないことにあります。
- 同一候補者が複数社に同時応募する: ATS内の1候補者が、実世界では競合他社にも応募している可能性があります。 ATSはあくまで「自社に対する応募行為」のみをモデル化しており、この限界を理解した設計が必要です。
- エンジニア自身がATSのAPIを使う場面: 採用システム連携(HRIS↔ATS)・HR Techプロダクト開発・採用データの分析基盤構築など、 エンジニアがATSのAPIを扱うケースは増えています。 Greenhouse Harvest APIやLever APIを使う際に、このデータモデルの理解が直接実装に影響します。
- Webhookのべき等性(idempotency)設計: ATSがWebhookで
candidate_stage_changedを送ってきた場合、 ネットワーク障害によって同一イベントが2回届くことがあります。 受け側のシステムはイベントIDによる重複検出(processed_eventsテーブル等)を実装して、 2回目の受信を無視できるようにしなければなりません。 「2回ステージが進む」というバグは採用データを壊すため、本番障害として扱う必要があります。
-- Webhookべき等性の実装例
CREATE TABLE processed_webhook_events (
event_id VARCHAR(255) PRIMARY KEY, -- ATSが送るユニークID
received_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
payload JSONB
);
-- 受信処理(疑似コード)
async function handleWebhook(event) {
const alreadyProcessed = await db.query(
'SELECT 1 FROM processed_webhook_events WHERE event_id = $1',
[event.id]
);
if (alreadyProcessed.rows.length > 0) {
return { status: 200, body: 'already processed' }; // 冪等に成功を返す
}
await db.transaction(async (tx) => {
await tx.query(
'INSERT INTO processed_webhook_events(event_id, payload) VALUES($1, $2)',
[event.id, event]
);
await processStageChange(tx, event); // 実際の処理
});
}ATSのデータモデルを理解することは、採用ドメインの「言語」を習得することです。 採用担当者が「ステージを進める」「スコアカードを提出する」と言うとき、 その背後にあるデータ操作を正確にイメージできるエンジニアが、 より良い採用システムを設計できます。
理解度チェック
GreenhouseとLeverのデータモデルの最大の違いはどれですか?
キーボード: 1〜4 で選択、Enter で回答