なぜデータモデルが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
  }
ATSの5コアエンティティとその関係。Job・Candidate・Application・Stage・Scorecardがコアドメインを構成する

各エンティティの主要フィールドと役割を整理します。

Job(求人)

採用ポジションを表すマスターエンティティ。statusopen / 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_recommendationstrong_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(ステージ)の役割の違いです。 両者は明確に異なる概念です。

概念役割値の例設計パターン
StatusApplicationの終端状態機械(State Machine)active / rejected / hired有限状態機械(FSM)
Stageパイプライン上の現在位置(どのステップにいるか)Screening / Interview / OfferJobごとにカスタム可能な順序付きリスト

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 note
ApplicationのStatus状態機械。active/rejected/hiredの3状態と遷移イベント。rejectedからの復活(unreject)をどう扱うかが重要な設計判断になる

unreject(復活)の設計は実装上の重要な判断点です。 「一度落とした候補者を別のポジションで再検討する」という現実のユースケースをどう扱うか。 Statusをそのまま active に戻すと、 「なぜ一度落とされたか」という履歴が消えてしまいます。 このためAudit Trailの設計が不可欠です。

Stageパイプラインの設計

Stageはパイプライン上の位置を表し、Jobごとにカスタマイズできます。典型的なパイプラインは以下の通りです。

Application Received
Screening
Phone Screen
Interview
Offer
Hired

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完全分離)大企業・金融・医療・公共機関
HybridSMBは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のデータモデルを理解することは、採用ドメインの「言語」を習得することです。 採用担当者が「ステージを進める」「スコアカードを提出する」と言うとき、 その背後にあるデータ操作を正確にイメージできるエンジニアが、 より良い採用システムを設計できます。

理解度チェック

問題 0 / 40%
Q1

GreenhouseとLeverのデータモデルの最大の違いはどれですか?

キーボード: 1〜4 で選択、Enter で回答