配信除外リストは、約束するのは簡単です。「止めてと言われたら、二度と連絡しません」はポリシーページの1文であり、エンジニアリングとしては1週間ほど、しかもその大半は予想しない場所にかかります。スキーマは半日です。難しいのは、フォーム営業の配信除外リストがアカウントをまたいで効き、すでに進行中の作業にまで遡って届き、そのうえで何を止めたのかを後から証明できなければならない点です。
01スキーマと、実際に効いているインデックス
配信除外は登録可能ドメインを単位にします。ホスト名でも、個々のメールアドレスでもありません。acme.com で止めてほしいと言った人は、www.acme.com や careers.acme.com なら聞いてもよいと同意したわけではありません。パブリックサフィックスリストで正規化し、小文字にし、先頭の www を落とし、国際化ドメイン名は Punycode 形式に変換してから保存します。1つの企業が2通りの表記で表に入っているのは、機能としては項目がないのと同じだからです。
- domain。正規化された登録可能ドメインで、照合に使われる唯一の列です。
- tenant_id。その配信除外が属するアカウント。プラットフォーム全体を表す番兵値を用意します。
- reason。自由記述で、必須、既定値なし。理由を言える人がいないなら、登録しません。
- source。どう届いたか。返信、フォームからの依頼、運用者の操作、一括取り込み、法的な請求。これらは後の扱いが異なり、真偽値1つにすると全部が失われます。
- evidence。それを引き起こしたメッセージやチケットへのリンク。
- created_by と created_at。誰が、いつ。
テナントとドメインに対するユニークインデックスが、再登録を安全にしています。書き込みは、キーが重複した場合に失敗するのではなく reason、source、更新時刻を上書きする挿入を通ります。ですから運用者は同じ40件のドメインを2回貼り付けても、40行が残り、エラーは出ません。冪等な再登録は、聞こえよりずっと重要です。そうでなければ、人は書く前に読むことになり、読んでから書く形こそ、このインデックスが取り除くために存在する競合そのものです。
02フォーム営業では、取り込み時ではなく全工程で照合する
パイプラインは、工程のあいだに実時間の空く一連の流れです。読み取りが取得元から届き、ゲートを抜けて通過になり、リサーチされ、下書きが書かれ、人を待ち、そこで初めてノックが配信されます。最初の工程から最後まで、何日も経ちます。企業はその窓の内側のどの時点でも、配信除外になりえます。
ですから発見時点での配信除外の照合は、それだけではほとんど無価値です。取り込み時が唯一の照合なら、火曜にオプトアウトした企業の下書きは、月曜に作られたまま承認キューに残っていて、その下書きは送られます。リストは正しく、項目もそこにあり、それでもメッセージは出ていきました。失敗はデータの中にはありません。
- 取り込み時。配信除外されたドメインが、そもそも読み取りにならないようにします。
- リサーチの前。リサーチは予算を使うので、連絡できない企業に使う意味がありません。
- 下書きの前。理由は同じで、かかっている費用はより大きくなっています。
- 承認キューを描画するとき。隠すのではなくブロック済みとして表示し、人が、何かが止まったことを見えるようにします。どこへ行ったのかと考えさせないためです。
- 配信の直前。リクエストが出る前に実行される最後の文として。
正しさを担っているのは、このうち最後の1つだけです。他の4つは作業を節約し、決して送れないものを承認するために人が注意を使うのを防ぎます。最後の照合は、実行単位のキャッシュではなくデータベースを読む必要があります。キャッシュは、ちょうどバグが住んでいる間隔の分だけ古いからです。数千行の表に対するインデックス付きの検索1回であり、最適化する価値はありません。
032つのワーカー、1つの企業
並行処理の事例がレビューを生き延びてしまうのは、絵にしにくいからです。2つのワーカーが同じ企業を拾います。最初のワーカーが気付かないうちにリースが失効した場合、あるいは1つの事業が異なるIDで2つの発見元から届き、正規化すると同じドメインになる場合です。両方が配信除外を照合します。両方が通ります。両方が下書きを書きます。承認キューには1社に対して2通のメッセージが並び、片方を見送っても、もう片方には何も起きません。
トランザクションを長くしても直りませんし、2回照合しても直りません。直すのはユニーク制約です。下書きの行は、テナント、ドメイン、キャンペーンに対するユニークインデックスを持つので、2回目の挿入はアプリケーションのロジックではなくデータベースの中で負け、負けたワーカーは重複キーをエラーではなく通常の結果として扱います。リースは claimed_at とワーカーIDを持つので、詰まったジョブは、単に遅いのではなく見える形になります。
配信は同じ考え方を反対側から使います。送信の工程は、現在の状態を WHERE 句に書いたうえで、下書きを承認済みから送信中へ移し、その更新がちょうど1行に効いた場合にだけ先へ進みます。1秒前に配信除外のイベントがその行を停止済みへ変えていれば、更新は何にも一致せず、送信は起きず、順序についてアプリケーションのコードが考える必要はありません。この比較して書き換える更新が、配信除外を「たいてい正しい」ではなく競合のない状態にします。
04配信除外は過去にも及ぶ必要があります
ドメインを配信除外にすることは、フィルターではなくイベントです。それが着地すると、そのドメインについて待機中のリサーチを取り消し、そのドメインのすべての下書きを、配信除外のIDを理由に記録したうえで停止済みへ移し、その行を承認キューから引き上げます。問題のすべては、すでに終わっている作業のほうにあります。これから読み取るものだけに適用されるフィルターは、終わったものをそのまま残し、そして終わったものだけが、送信に手が届く距離にいます。
停止済みの行は、その後も残ります。削除しません。ここは議論になる部分です。削除するほうが礼儀正しい選択に感じられるからです。しかしそれは、何かが止まったという唯一の証拠を壊します。ある企業から、こちらが連絡していないかと問い合わせが来たとき、誠実な答えは具体的です。9日に下書きが作られ、11日にドメインが配信除外になった時点で停止し、一度も配信されておらず、その本文はこれです。この答えには、行が存在している必要があります。
ですからルールは双方向に成り立ちます。見送りの記録は決して削除しません。見送った読み取り、停止した下書き、配信除外の項目はすべて保存し、理由の欄は3つとも必須です。監査できないフィルターはただのリストであり、リストならすでに全員が持っていると主張しています。
05顧客リストは配信除外リストではありません
最後の誤りは、片付けのように見えます。テナントは既存顧客の一覧をすでに持っていて、そこへ営業をかけたくない。そこで誰かがその一覧を配信除外の表に読み込みます。1週間はうまくいき、そのあと情報が消えます。
これらは企業についての別々の事実です。配信除外は、人が止めてほしいと言ったことを意味します。あらゆるものに優先し、期限がなく、メッセージの内容を問わず適用されます。すでに顧客であることは、新規の見込み客として扱うなという意味であって、同じ指示ではありません。担当チームは拡販の会話を望むかもしれず、望んだ場合、配信除外の表は、それを一度も求めていない人の代わりに断っていることになります。競合の除外は3つ目の別物です。商談の進行中に営業がかける保留は4つ目で、他と違って終了日があります。
分けておけば、「この企業になぜ一度も連絡していないのか」という問いには、クエリ1本で出せる一言の答えがあります。自由記述の理由の裏で1つの表に混ぜてしまうと、答えは誰かが文章を読んで推測することになり、1か月もすれば理由の列には「顧客、削除しないこと」のような値がたまり始めます。表を分け、意味を分け、配信の時点で、そのすべてを参照する共通の照合を1つ置きます。
現時点での配信は、相手企業自身の公開された問い合わせフォームを意味します。メール送信は2026年第4四半期の予定で、未実装です。そのおかげで上記のすべてが静かに単純になっています。配信除外すべきチャネルが1つしかないからです。2つになれば、ここで述べたすべての照合が両方で走り、そして過去に遡る部分が、最も慎重さを要する場所になります。