こんにちは!ウォータールー大学のコンピュータサイエンス専攻のジョセフです。2025年9月から12月にかけて、Slash社でソフトウェアエンジニアリングのインターンシップを終えました。このブログを簡単にまとめると——本当に充実した経験でした。皆さんもぜひ参加すべきだと思います。読んでくれてありがとう。

さて、今回は本気です。これまで音楽関連の小さなシードスタートアップから大手テック企業まで様々な会社でインターンを経験してきましたが、Slashでは他では得られなかった「集中力」と「主体性」が絶妙に融合した環境でした。 チームは優秀で結束が固く、Kubernetesの運用からDBパフォーマンスまで、多くの複雑なテーマについて深く学ぶことができました。さらに重要なのは、本当に興味深い課題に取り組む機会を得られたことです。だからこそ、この経験をブログに綴ろうと決めたのです!

スラッシュの事業内容を簡単にまとめると、起業家やスニーカー転売業者向けの金融プラットフォームとしてスタートしたが、現在では法人カード、国際送金、暗号資産口座などを提供する総合的な銀行系フィンテック企業へと成長している。 スラッシュを初めて知った時、私はこうしたことをほとんど知りませんでした。何しろ私はスニーカーマニアでもなければ、フィンテックに詳しいわけでもなかったのです。そのため、この機会が本当に価値あるものかどうか、長い間考え込みました。しかし、このブログ記事からもお分かりいただけるように、参加を決めたことは、私のキャリア初期において下せた最良の決断の一つとなりました。

私が最終的に他の企業ではなくSlashを選んだ主な理由は以下の通りです:

  • それはシリーズBの企業だった – これほどの規模のスタートアップで働いたことがなく、その環境を体験したかったのです。
  • 年間売上高は1億5000万ドルに達しており、シリーズB段階の企業としては驚異的な高さである – 彼らは単に生き延びたり、辛うじて持ちこたえているだけではない。積極的に規模を拡大していたのだ。さらに言えば、それは多くの小規模スタートアップが欠いている優れた設計思想と技術的厳密性を備えていた可能性を示唆している。
  • 彼らは活発に成長していた – 同社は1年で収益を6倍に拡大し、最近シリーズBの資金調達を完了した。まさに成長の黄金期にあり、私はそうした勢いのある企業で働きたいと考えていた。
  • 若くて活気あふれるチームです – インタビューを通じて、ここの人々は気さくで話しやすく、共感できる仲間と協力して何かを築けることにワクワクした。
  • それは小規模でスリムな会社だ – 1億5000万ドルのARRを約12名のエンジニアチームが達成した件、お伝えしましたか? 1人あたり1250万ドルのARRです パー エンジニアって、すごいよね。明らかに、スラッシュの人たちは一流の才能の持ち主で、私は高い所有権と素晴らしい指導の両方を得られるだろう。
  • インターンシップとしては報酬と中小企業ならではの特典が最高レベル… 🤫

要約すると環境、才能ある人々、そしてスタートアップのエネルギーが、成長に最適な場所を提供してくれると思ったのです。インフラ構築からスケーラビリティを考慮した計画立案、システム設計の検討など、プロジェクトを端から端まで担当する機会を得られると考え、このチャンスを掴んだのです!

プロジェクト: 通知 V2

スラッシュについてすぐに学んだ特別な点があります——重要なプロジェクトをインターンに割り当て、彼らが最後までやり遂げられると信頼していることです。私は2週目に、カード取引の拒否通知システムの刷新を任されました。難しそうには見えませんよね?しかし、既存の通知システムにはいくつかの問題がありました:

  • サービスの中核にビジネスロジックを組み込み、内部イベントシステムとの緊密な連携を実現
  • 使用していたプロバイダーによるメール指標の低さと、さらに深刻なメール配信率の悪化
  • 開発者体験が困難であったため、エンジニアはシステムを完全に迂回し、プロバイダーのAPIを直接呼び出すようになった

そして、厄介な課題が訪れた——より優れた退会通知メールを構築するには、単なる修正では不十分だった。通知システム全体の全面的な再設計が必要であり、私はそのプロジェクト全体のデザイン、開発、展開、スケーリングの責任者(DRI)となった。それは非常に気が重かった…しかし同時に、最終的に数百万件の通知を処理するシステムを設計する絶好の機会でもあった。私は飛び込むことに胸を躍らせた。

以前の通知システムを研究し、エンジニアと議論した結果、この新システムが遵守すべきいくつかの基本原則を明らかにしました:

  1. イベント非依存設計 – サービスは、単発スクリプトでもイベントハンドラーでも、どこでも動作できるべきである
  2. プロバイダー非依存設計 – 最小限の労力で既存プロバイダーを簡単に削除し、新たなプロバイダーを導入できる機会は計り知れない価値がある。
  3. 開発者優先設計 – フラストレーションによる上書きはもう終わり。開発体験は可能な限り直感的で簡単であるべきだ。
  4. 観測可能かつスケーラブル - 通知にはライフサイクル全体を追跡する機能が必要であり、最終的には月間300万通のメールをサポートできるようにすべきです!

これにより、通知V2のハンドラーベース設計が誕生しました。このシステムは通知ハンドラーを中心に構成されており、特定の外部チャネルプロバイダー(例:メール用Resend、SMS用Twilio)向けに、通知送信やWebhook処理といった共通のハンドラー機能を公開しています。

システムの中核をなすのは 通知インテント通知のすべてのデータ(プロバイダー、チャネル、ペイロード引数など)を運ぶ汎用的な一時オブジェクトです。各ハンドラーは自身のインテントの形状を定義し、これらのオブジェクトは強タイプ化されているため、静的型チェックと内省をサポートします。これにより、開発者は各プロバイダーの複雑な詳細をすべて知る必要がなく、IDEの自動補完を活用し、型チェックが確実に機能することを信頼できます。

This example briefly illustrates how to use the service to send a variety of email notifications for an outgoing payment. All these create functions create notification intents, and they are strongly typed to suit the provider (in this case, Resend)
const sendOutgoingPaymentNotification = async (context: {
 transactionId: string;
 amount: number;
 recipientEmail: string;
 alertEmail: string;
}) => {
 const { transactionId, amount, recipientEmail, alertEmail } = context;
 const { originalSenderId } = getOriginalSenderForTransaction(transactionId);


 await notificationsV2Service.sendNotifications([
   // We can create a one-off notification easily...
   ResendNotification.create({
     email: {
       recipient: alertEmail,
       content: {
         subject: "INTERNAL ALERT: An employee just sent an outgoing payment",
         body: `An employee just sent an outgoing payment of $${amount}.`,
       },
     },
   }),
   // Or use a React Email template for pretty emails..
   ResendNotification.create({
     email: {
       recipient: recipientEmail,
       content: YouJustReceivedPaymentTemplate.withProps({ amount }),
     },
   }),
 ]);


 // Or send to specific users, respecting their preferences + registered emails
 await notificationsV2Service.sendNotificationsToUsers({
   users: [originalSenderId],
   preference: NotificationPreferences.PaymentConfirmation,
   createIntents: (user) => [
     ResendNotification.create({
       email: {
         recipient: user.registeredEmail,
         content: PaymentConfirmationTemplate.withProps({ amount }),
       },
     }),
   ],
 });
};

ユーザー設定の確認などのビジネスロジックが疎結合な方法で実装されることを保証するため、開発者が送信関数を呼び出すと、インテントは 通知ミドルウェア まずパイプライン。通知ミドルウェアは高階関数であり、共有DB検索を可能にする前処理ステップと、通知インテントを許可・変換・破棄する処理関数を備えています。この設計により、基盤システムを1つ使用して検証ロジックを定義・共有する、異なる送信関数を定義できるようになりました。例:

  • 基本的な「通知を送信」機能を定義できます。この機能では、インテントが 受信者禁止 ミドルウェアは、受信者情報(メールアドレス、電話番号)がカスタム禁止リストに登録されている場合、インテントを破棄する
  • より複雑な「ユーザーに送信」機能を定義できます。この機能では、意図が同じフローを通過します。 受信者禁止 ミドルウェアおよび ユーザーの好みを尊重する ミドルウェアは、ユーザーがそのチャネルでの通知をオプトアウトしている場合、インテントを破棄する

追加の利点として、将来の開発者が複雑な関数の迷路を追跡することなく、新しい送信関数の追加・削除・変更を容易に行える。


私たちが採用したフローの例です。sendNotificationToUsersとsendNotificationの両方がヒューリスティックスミドルウェアを使用している点に注意してください。ただし、ユーザー送信関数ではよりユーザー固有のバリデータが実行されます。


ミドルウェア処理の後、サービスは作成する 通知 Temporalワークフローを開始する前にライフサイクルオブジェクトを処理する。 Temporalの優れた点は、ワークフロー障害に対する組み込みの再試行機能、永続的な状態管理、およびオーケストレーション保証を提供することです。これらはすべて通知の配信を確実にするために重要です。ワークフローは通知をキューに格納し、プロバイダーのレート制限を遵守しユーザーへのスパム送信を防ぐために送信をスロットリングし、その後バッチをプロバイダーハンドラーに渡します。ハンドラーはプロバイダーへの通知送信を担当し、サービスはそれに応じてライフサイクルを更新します。


外部プロバイダーからWebhookレスポンスが送信されると、ハンドラーはそれらを標準化されたデータ型に変換し、型付きアクションを発行します。アクションは、通知を受信した際にイベントに対応するための構造化され型安全な方法を提供すると同時に、サービスを可能な限り中立的な状態に保ちます。例えば、

  • 受信者を更新するアクションを用意しています。これにより、ハンドラーはプロバイダーとチャンネルが「禁止」と判定した内容に基づき、受信者を「恒久的に禁止」状態に更新するか否かを制御できます。
  • 通知の再試行アクションを用意しています。例えば、メールが一時的にバウンスした場合、そのメールを指数関数的に再試行します。ハンドラーがメールプロバイダーの応答を「バウンス」と判断した場合、このアクションをトリガーできます。

ProcessWebhook returns the same body structure, allowing specific webhook logic to be handled by the actual provider itself.
const statusMap: Record<string, string> = {
 bounced: "failed",
 // more statuses here...
};


const ResendHandler = createHandler({
 // This is a simple example of something we could do
 processWebhook: (webhookBody: ResendWebhook) => {
   const notificationId = extractIdFromWebhook(webhookBody);


   const newInternalStatus = statusMap[webhookBody.state];
  
   const actions = newInternalStatus === "failed"
     ? [{ action: "retryNotification", notificationId }]
     : [];


   return {
     status: newInternalStatus,
     actions,
   };
 },
});

ふう!これで流れの大半は説明できた。途中で他にもいくつか興味深い技術的課題に遭遇したんだ、例えば

  • 指数関数的再試行のサポート – 既に実行された再試行回数に基づいて、Temporal内で将来の再試行ジョブをスケジュールします
  • 優れたトレーサビリティ – 例えば、トレースIDや通知とその再試行を 通知セット エンティティ
  • バッチ処理 – 個別送信とバッチ送信の両方を統合することで、プロバイダーのレート制限超過を回避し、開発者が通知の送信方法を制御できるようにします。

新入エンジニアの皆さんは、入社後にコードで詳細を見つけてください😉

プロジェクト:フェイスリフト

プロジェクト・フェイスリフトは、当社チームによるSlashウェブ体験のエンドツーエンド再設計でした(エンジニアブログ参照) アルベルト・ティアン、フェイスリフトの父によるフェイリフトの詳細については深く掘り下げませんが(素晴らしいエンジニアブログを参照)、その核心は、大規模なフロントエンド全体に新しいコンポーネントライブラリとTailwindを導入することでした。ローンチ準備として、エンジニアリングチームは少し遊び心を持ち、集中的なハックウィークを実施することに決めました! ただしこれはオフィスで行う典型的なハックウィークとは異なり、数台の車に分乗してヨセミテ近くのバス湖畔にある山小屋まで3時間ドライブし、一週間をかけて構築・リファクタリングを行いながら、心から互いに楽しみながら過ごしたのです。

今週は、バックエンド作業から完全に移行し、取引と紛争処理フローの刷新を支援しました。これは主にフロントエンド作業であり、これまでの業務とは一風変わった新鮮な変化でした。また、次のようなフロントエンド設計インフラストラクチャの多くを横断して作業する機会にもなりました:

  • コアプリミティブ例えば、サイドパネル、モーダル、ポップアップをどのように生成するか
  • 再利用可能な組み立て式コンポーネント例えば、 選択可能な検索
  • テーブルマネージャー: 並べ替え、フィルタリング、ページネーション、検索が簡単にできるテーブルを作成するためのカスタムコンポーネント
  • 堅牢なサブフォーム検証: Arktype、Tanstackフォーム、および自社開発のモデル生成システムを使用

サイト刷新は複雑な製品ロジックよりも、チームが一つのプロジェクトに集中して結束した際の驚異的なスピードを目の当たりにする機会でした。一週間でサイト全体が変貌していく様子を、あの小屋で同僚たちと協力し絆を深めながら見守れたことは、間違いなくインターンシップのハイライトでした。ただし刷新作業はその週で終わりではありません——その後数週間はサポートチームと連携し、製品の細かい不具合を徹底的に修正することで、ローンチが円滑に進むよう尽力しました!

プロジェクト:テンプレート

毎学期末、私の大学では簡単な質問が書かれた小さなアンケートを送付するのが習慣です――「授業で学んだ内容が仕事にどの程度役立ちましたか?」。さて、Slashでの勤務期間中、実際に授業で学んだ概念を直接応用する機会があったのです!というのも、トランザクションメールを新デザインのFaceliftに統一したかったため、通知システムがカスタムテンプレートをサポートするのが理にかなっていたのです。 フロントエンドと同じ配色と余白を保証するため、最終的にReact Emailというメールテンプレートライブラリを用いてコードでこれらのテンプレートを構築しました。これによりJSXをテンプレートに使用できるのです。これは我々にとって完璧な解決策でした。チームがReact JSXに精通しているため、ウェブとメールの両方で同じTailwind CSS v4デザインシステムを使用することで、ブランドの一貫性を確保できたのです。

ただし、一つ注意点がありました。TailwindCSS 4の主要な利点の一つは、従来のJS設定オブジェクトの代わりにカスタムCSSファイルを使用できる点であり、私たちのチームはこの機能を活用していました。しかし、React EmailのTailwindコンポーネントは実際には 必須 JS設定オブジェクト

つまり、トランスパイラを作ることになったんだ!

@import "tailwindcss";

@import "../custom-styles.css";

@custom-variant dark (&:where(.dark, .dark *):not(:where(.light, .light *)));

@theme {
  --color-*: initial;

  --spacing-base-scale-0: 0px;
  --spacing-base-scale-1: 1px;
  --spacing-base-scale-2: 2px;
  --spacing-base-scale-4: 4px;
  --spacing-base-scale-6: 6px;
  --spacing-base-scale-8: 8px;
  --spacing-base-scale-12: 12px;
  --spacing-base-scale-14: 14px;
  --spacing-base-scale-16: 16px;
  --spacing-base-scale-20: 20px;
  --spacing-base-scale-24: 24px;
  --spacing-base-scale-28: 28px;
  --spacing-base-scale-32: 32px;
  --spacing-base-scale-36: 36px;
  --spacing-base-scale-40: 40px;
  --spacing-base-scale-44: 44px;
  --spacing-base-scale-48: 48px;
  --spacing-base-scale-52: 52px;
  --spacing-base-scale-56: 56px;
  --spacing-base-scale-60: 60px;
  --spacing-base-scale-64: 64px;
  --spacing-base-scale-68: 68px;
  --spacing-base-scale-72: 72px;
  --spacing-base-scale-76: 76px;
  --spacing-base-scale-80: 80px;
  --spacing-base-scale-84: 84px;
  --spacing-base-scale-88: 88px;
  --spacing-base-scale-92: 92px;
  --spacing-base-scale-96: 96px;
  --spacing-base-scale-100: 100px;
  --spacing-base-scale-104: 104px;
// WARNING: This file is generated by a script. Do not edit it manually.
// Generated from: ./lib/tailwind.build.css
// To regenerate: yarn build:tailwind-config

import type { GenericAny } from '@slashfi/slash-base';

const plugin = require('tailwindcss/plugin');

export default {
	theme: {
		extend: {
			backgroundColor: {
				"surface-subtle": "#f9f8f7",
				"surface-neutral": "#ffffff",
				"neutral-subtle-default": "#fcfcfb",
				"neutral-subtle-hover": "#f9f8f7",
				"neutral-subtle-active": "#f9f8f7",
				"neutral-subtle-disabled": "#fcfcfb",
				"neutral-normal-default": "#f9f8f7",
				"neutral-prominent-default": "#f4f3f1",
				"neutral-muted-default": "#e9e7e3",
				"neutral-bold-default": "#958f82",
				"neutral-bold-hover": "#6d685f",
				"neutral-bold-active": "#6d685f",
				"neutral-bold-disabled": "#bdb7aa",
				"neutral-muted-hover": "#e0ddd6",
				"neutral-muted-active": "#d9d6ce",
				"neutral-muted-disabled": "#efeeeb",
				"neutral-prominent-hover": "#efeeeb",
				"neutral-prominent-active": "#efeeeb",
				"neutral-prominent-disabled": "#fcfcfb",
				"neutral-normal-hover": "#f4f3f1",
				"neutral-normal-active": "#f4f3f1",
				"neutral-normal-disabled": "#fcfcfb",
				"neutral-bolder-default": "#6d685f",
				"neutral-boldest-default": "#32312c",
				"neutral-boldest-hover": "#32312c",
				"neutral-boldest-active": "#32312c",
				"neutral-boldest-disabled": "#bdb7aa",
				"neutral-bolder-hover": "#32312c",
				"neutral-bolder-active": "#32312c",
				"neutral-bolder-disabled": "#bdb7aa",
				"interactive-neutral-subtle-default": "#4b3a1c",
				"interactive-neutral-normal-default": "#4b3a1c",
				"interactive-neutral-prominent-default": "#4b3a1c",
				"interactive-neutral-prominent-hover": "#4b3a1c",
				"interactive-neutral-prominent-active": "#4b3a1c",
				"interactive-neutral-prominent-disabled": "#4b3a1c",
				"interactive-neutral-normal-hover": "#4b3a1c",
				"interactive-neutral-normal-active": "#4b3a1c",
				"interactive-neutral-normal-disabled": "#4b3a1c",
				"interactive-neutral-subtle-hover": "#4b3a1c",
				"interactive-neutral-subtle-active": "#4b3a1c",

より正確に説明すると、ビルドステップ中に、デザインシステムのCSSファイルから抽象構文木(AST)を構築するスクリプトを追加しました。このASTはCSSルールの構造化された表現を提供し、ファイルを簡単に解析して以下の処理を可能にしました:

  • サポートされていないCSSクラスを除外するたとえば、ホバースタイルやポインタユーティリティは、ほとんどのメールクライアントでは機能しません。
  • 互換性のあるカラーシステムに変換する: カラー定義にはOKLCHを使用しましたが、メールクライアントでは表示できないため、HEXに変換しています。
  • TS構成オブジェクトを構築するこれはJSにコンパイルされ、その後コードベースのどこにでも簡単にインポートできます。

このトランスパイル処理は、デザインシステムにおける単一の情報源を迅速に実装する方法でした。色を微調整した場合、再ビルド時に即座にトランスパイルされるため、ウェブやメールなど全ての媒体で一貫性が保たれます。 さらに、モバイルアプリケーションなどの他のクライアントも、必要に応じてこのトランスパイル済みオブジェクトを利用できました。生成された設定オブジェクトを活用し、ウェブコンポーネントを反映したメール用共通Reactコンポーネント群を構築できました。例えば: タイポグラフィ そして ボタン コンポーネントは、ほぼ同一のプロパティを備えています。これらすべてが開発者体験の向上に寄与し、テンプレート作成の負担を軽減しました!

最後に、通知テンプレートも社内レジストリフレームワークを活用するようにしました。これは当社が構築したカスタムデータ構造です(詳細は このブログ記事簡単にまとめると、レジストリはテンプレートの定義、データベースからのシリアル化とデシリアル化、そしてビジネスロジックの統合的な管理を型安全な方法で実現します。例えば、 カード承認通知テンプレート.tsx カード認証イベントハンドラーのコードのすぐ隣に配置されており、通知対象のイベントを素早く確認できるのが素晴らしい!このプロジェクトでレジストリを活用できたのは非常に有益でした。チームが優れた再利用可能なコードを書くことに注力している姿勢が如実に表れています。


私は何を学んだのか?

参加する前はユニークな経験になると思っていましたが、参加を決めて本当に良かったです。Slashでは、これまでのどの役職よりも、オーナーシップ、プラットフォーム設計、エンジニアリングの速度について多くを学びました。特に得た重要な気づきは以下の通りです:

  • 信頼は計り知れない価値がある – Slashには毎日のスタンドアップ会議がない。週次スプリントやバックロググルーミングもない。 週に1回のミーティングがあり、それはチーム全体に構築したものをデモするためだけです。これが機能するのは、チーム全体が強いコミュニケーション能力と印象的なオーナーシップスキルを持っているからです。メンターシップがないわけではありません——私は自身の課題を解決するために週次1対1の面談を行っています——しかし、Slashはあなたが最善の仕事をすることを信頼し、可能な限り集中できる環境を育んでいるのです。
  • チームは素晴らしい – 他のインターンシップでは同僚はプロフェッショナルでしたが、ここでは同僚は 私の民スラッシュには間違いなく素晴らしい文化があります。楽しく、支え合い、一緒に働きやすい人ばかりです。一緒に働いて楽しくなかった人や、気後れした人は一人もいませんでした。しかも、彼らはあなたを正社員と同じように扱ってくれます。さらに、週末には一緒にバスケットボールをプレイしに行くこともできます😃
  • インターンとして、あなたはここで他のどこよりも飛躍的に成長できるでしょう – 他の企業であれば、インターンに中核的な通知システム全体をプロジェクトとして任せることはなかっただろう。困難で成長を促すような課題は、既に解決済みか、あるいは上級エンジニアによって解決中だったはずだ。Slashの独自性は、スリムなチーム編成、真にビジネス上重要な課題、そしてインターンであっても重要な仕事を任せる文化が組み合わさっている点にある。

このインターンシップでのその他の楽しい思い出をいくつかご紹介します:

  • 1週間で合計20ポンド(約9kg)ものステーキを食べた…ハックウィークに感謝!
  • プール競技で同期のインターンを完膚なきまでに叩きのめした直後、正社員にあっさり屈辱を味わわされた。
  • ついに1プレートをベンチプレスできた!
  • 私は「オフィス全体の大食い」の主要な貢献者です(ついでに言うと、シェイクシャックが大好きだと気づきました)。

この4ヶ月は予想以上にあっという間に過ぎ去り、Slashでの経験に心から感謝しています。間もなく卒業を迎えますが、1ヶ月後、1年後、10年後に人生がどんな道を用意していようとも、Slashでのインターンシップが、どこへ行こうとも成功するためのノウハウと自信を与えてくれたと確信しています。

もしもっとお話ししたいなら、 LinkedInで気軽に連絡してください! ご質問がございましたら、喜んでお答えいたします。

Read more from us