有名テック企業の技術ブログを、ひとつのフィードで。
フィード
10件
この記事は「Eureka Advent Calendar 2025」25日目の記事です。はじめにはじめまして、BI teamのhoppy(@its_my_hoppy)です!気づいたら年末に差し掛かっており、今年も色々やったなと振り返りながらこの記事を書いています。データ基盤を運用していると、「データに異常がありそう」という事態に遭遇することがあります。例えば、日別で見ているデータが普段よりも激増・激減していたり、特定の日のデータが存在していなかったりといったケースがあると思います。日常的にダッシュボードなどで確認されるデータであればすぐに気づくことができるのですが、そうでないものに関しては異常があっても気づけない・気づくことが遅れてしまう場合があります。特にビジネスに直接影響するケースでは、早期に検知できる仕組みが重要です。この記事では、実際にデータの異常によって発生したインシデントを経験したことをきっかけに、dbtとElementaryを使ってデータ監視基盤とその運用を構築した事例を紹介します。Elementary導入する際の技術的な実装や細かい挙動の話はたくさんネットに落ちているので、組織としてどう取り組んだかという観点に重きをおいてお伝えします。対象読者dbt環境で、データ品質監視を導入したい方関係者を巻き込んでデータ品質監視に取り組みたい方背景何が起きたかビジネスチームから「主要指標の数値が悪化している」という報告があり、調査の結果、原因はイベントログの欠損でした。そのときどのような影響があり、そしてそこからどのような課題を認識したのかをまず整理します。影響この欠損は約2週間検知できず、ビジネスチームは業務の運用にこのデータを使っていたため、欠損期間中は正しい運用ができず、ビジネスに大きな影響が出ました。課題認識このインシデントを振り返り、以下の課題が明らかになりました。ガードレールの不足:リリース前にログ送信を検証する仕組みがなかった早期検知の仕組みがない:データが欠損しても気づく手段がなかったガードレールについてはクライアントチーム側でQA項目の追加などが進められましたが、早期検知についてはデータ側で仕組みを持つべきという結論になり、監視基盤の構築に着手しました。そこで、まずは「どのような状態を目指したいのか」を整理しました。Elementaryを選んだ理由元々、dbtの実行結果はDatadogで監視していましたが、エラーが発生した際は生ログを確認する必要があり、「何が」「なぜ」失敗したのかを把握するのに手間がかかっていました。この機会にobservability toolとしてElementaryを導入することにしました。Elementaryを導入した理由は以下の通りです。1. dbt環境ですぐに利用できる既存のdbtプロジェクトにパッケージを追加するだけで始められる2. OSS利用であればコストなしで始められるまずは小さく試して、効果を確認できる3. Slack連携で柔軟に通知先を設定できるテストごとに通知先チャンネルやメンション先を設定できる「このデータに異常が出たら、このチームに知らせる」という運用を仕組み化できる4. 失敗内容がSlackで把握できる生ログを見に行かなくても、どのモデル/テストがどんな理由で失敗したかがわかる取り組みの進め方監視基盤の構築は、技術的な実装だけでなく、関係者を巻き込んで進めることが重要でした。以下のステップで進めました。Step1: ステークホルダーの特定と巻き込みまず、データ欠損が発生した場合に誰が影響を受けるか、誰が対応するかを整理しました。これらの関係者を巻き込み、「どのデータを監視すべきか」「検知したらどう動くか」を一緒に決めていきました。Step2: 監視対象の決定すべてのデータを監視するのは現実的ではありません。ビジネスインパクトの大きいデータから優先的に監視することにしました。ビジネスチームと協議し、「これは欠損すると困る」というデータを特定しました。Step3: 閾値の設計次に、「監視したいイベントがどのくらい減少したらアラートを出すか」を決めました。閾値の設計では、誤検知と見逃しのバランスが難しいです。閾値が厳しすぎる → 誤検知が多くなり、アラート疲れを起こす閾値が緩すぎる → 本当の異常を見逃す最終的な方向性としては、そもそも監視されていなかった指標だったので多少誤検知が起きたとしても意識が向くほうが大事、という形で閾値を設定しました。細かい調整は運用していく中で決めていく形でもいいと思います。Step4: 対応フローの策定検知した後の動き方も事前に決めておきました。1. テスト失敗(異常検知) ↓2. Slack通知 - 関連チームに自動でメンション - 確認用ダッシュボードのリンクから実数値確認 ↓3. Incident起票 - 調査が必須となるため、Incidentとして扱う ↓4. Incident対応チャンネルで調査 - 影響を受けるチーム(ビジネスチーム等)を招集 - 原因になりうるチーム(クライアント開発等)と連携 - 原因特定・対応ポイントは以下の3点です。1. 検知したら関連チームに自動でメンションが飛ぶデータチームだけでなく、影響を受けるチームや対応しないといけないチームにも即座に情報が届きます。これにより、「データチームが気づいて連絡する」というタイムラグがなくなりました。2. 検知したら必ずIncidentとして扱う検知した時点でIncident起票して調査を開始します。結果的に問題なかった場合はクローズすればよく、本当に問題だった場合は初動が早くなります。異常が発生した際のビジネス影響が特に大きいものだったので、発生した際に放置されないで調査に進めていくためにこのようにしました。3. アラートが来たら誰が最初のアクションを取るかを明確にしておく「誰かが対応するだろう」ではなく、最初のアクションを取る責任者を決めておくことで、アラートが放置されることを防ぎます。テストの実装今回のケースでは日別で最新のデータの集計値と過去の集計値の傾向を比較したいという要件だったので独自でテストを書いて適用しました。実装例{{ config( severity='error', meta={ 'owner': '@data-team', 'subscribers': ["@client-team", "@business-team"], 'description': 'hogeイベントが急減しています\n'。 'ダッシュボードを確認してください: https://...', 'channel': 'alert-data-quality' } )}}WITH aggregation AS ( ...)SELECT ...WHERE ...ポイントはmetaの設定です。owner: 監視の責任チームsubscribers: 検知時にメンションする関係者channel: 通知先のSlackチャンネルdescription: 何が起きているか、どこを見ればいいかの説明本来であればテスト内容(どのような条件でテストしているか)を説明する用途のフィールドだと思いますが、今回は「何が起きていて、次に何をすればいいか」がすぐに分かる内容を書くようにしましたまた実際にデータがどうなっているのかを確認するためのダッシュボードのリンクを貼ってすぐに確認できるようにしましたこれにより、テストごとに誰に知らせるべきかを定義できます。Elementaryがこのmeta情報を読み取り、コマンド実行後に失敗した内容をSlackへ通知してくれます。configをSQLファイル内でなく、yamlに切り出して記述する選択肢もあったのですがテストの内容(ロジック)とconfigが1ファイルで確認できたほうがいいという結論からこのように実装しました。Slackへの通知dbtのテストが実行された後、Elementaryの edr monitor コマンドを実行することでSlackに通知が送られます。以下のコマンドをテスト実行後に叩いて通知を飛ばしています。edr monitor \ --profiles-dir <profiles_dir> \ --profile-target <target> \ --slack-token <token> \ --slack-channel-name <channel>引数の説明:— profiles-dir: dbtのprofiles.ymlがあるディレクトリ— profile-target: 使用するprofileのtarget(dev/prod等)— slack-token: Slack Bot Token— slack-channel-name: 通知先のデフォルトチャンネル導入してみて良かった点1. 関係者を巻き込んだことで、実効性のある監視になった「データチームが勝手に作った監視」ではなく、ステークホルダーと合意した監視になった検知時の対応フローが明確なので、アラートが形骸化しない2. 通知先を柔軟に設定できるテストごとに関連チームを指定できる「誰に知らせるべきか」を仕組み化できた3. dbt runの結果もSlackで把握できるようになったElementaryはテスト結果だけでなく、dbt runの結果も通知してくれるのでモデルがエラーになった場合、どのモデルがどんな原因で失敗したかがSlackでわかる以前は実行ログを直接確認する必要があったが、Slackで即座に状況を把握できるようになったので、トリアージを行うメンバーからも実際に便利になったという声が聞けた4. 既存のdbtワークフローに自然に組み込めた新しいツールを別途運用する必要がないdbtに慣れているメンバーならすぐに理解できる今後の課題1. 閾値のチューニング現在は一律の閾値だが、データによって適切な閾値は異なる可能性運用しながら調整していく2. 監視対象の拡大現在は最優先のデータのみ他の重要なデータにも監視を広げていきたいまとめデータ欠損インシデントをきっかけに、Elementaryを使ったデータ監視基盤を構築しました。ただ検知するだけでなく継続的に運用されるような監視体制を作るには以下のプロセスが重要だと考えます。ステークホルダーの特定と巻き込み誰が影響を受け、誰が原因になりうるかを整理監視対象の優先順位付け「これだけは死守したい」を関係者と合意アラートの重要度について関係者間で認識を揃え、放置されないようにする閾値の設計誤検知と見逃しのバランスを考慮対応フローの策定検知後の動き方を事前に決めておくことでスムーズなトリアージにつながるさらに、今回構築した仕組みを 横展開してさまざまなケースに広げていける体制を整えることで、長期的なデータ品質の向上にもつながります。データ品質の監視は地味に見えますが、問題が起きたときのインパクトを考えると非常に重要です。同様の課題を抱えている方の参考になれば幸いです。ここまで読んでいただきありがとうございました!みなさま良いお年を。dbt × Elementaryで”放置されない”データ監視の運用体制を作った話 was originally published in Pairs Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.
この記事は、「Pairs Engineering Advent Calendar 2025」24 日目の記事ですはじめにWeb フロントエンドチームの びゅん太郎 です.3 年連続 12/24 にアドベントカレンダーを投稿させていただいています今回は,バックエンドとフロントエンドが同レポジトリに共存かつクライアントアプリケーションが同レポジトリに 2 つ存在するという個性あふれるモノレポ環境 におけるフロントエンド開発の取り組みについて紹介します.この環境で,開発効率の最大化を目指し,API 型定義の自動生成 や AI コーディングエージェントの積極的な活用 を行いました.また,1 つのリポジトリ内で性質の異なる 2 つのクライアントアプリケーションを開発するという要件に対し,モノレポ構成での型安全性の維持やコンポーネントの共通化など,アーキテクチャ面でもいくつかの工夫を行いました.本記事では,これらの取り組みの中で特に効果的だった点や,開発中に直面した課題とその解決策について共有したいと思います.プロダクトの技術スタック本記事で取り上げるプロダクトは,モノレポ構成を採用しており,バックエンドとフロントエンドのコードが同じリポジトリで管理されています.クライアント技術スタックフロントエンドの開発には以下の技術スタックを採用しています.フロントエンドの技術スタック実は昨年のアドベントカレンダーでも TanStack Router についての記事 を投稿しており,弊 Web チームはこのライブラリに対しての信頼が高いですねクライアント構成このリポジトリの特徴として,2 つの異なるクライアントアプリケーション (以下,アプリA, アプリB) が存在します.これらは client ディレクトリ以下で管理されています.バックエンド含めたリポジトリ全体の主なディレクトリ構造は以下の通りです.├── client/ // client ディレクトリ│ ├── src/│ │ ├── アプリA/│ │ │ ├── api/│ │ │ ├── routes/│ │ │ └── routeTree.gen.ts│ │ ├── アプリB/│ │ │ ├── api/│ │ │ ├── routes/│ │ │ └── routeTree.gen.ts│ │ └── libs/│ ├── package.json│ ├── tsconfig.json│ ├── tsconfig.base.json // 共通設定│ ├── tsconfigA.json // アプリ A 用│ └── tsconfigB.json // アプリ B 用├── openapi/│ └── proto/│ ├── アプリA/│ │ └── v1/│ └── アプリB/│ └── v1/└── server/ // backend ディレクトリclient ディレクトリ内の構造について補足します.package.json は1つであり,依存ライブラリは両方のクライアントで共有しています.アプリ A と B は UI や基本的な機能は同じであるため,使用するライブラリも共通で管理したいと考えたためです.しかしルーティング定義ファイルである routeTree.gen.ts は,それぞれのクライアントディレクトリ内に個別に生成されます.また,それぞれのクライアントで利用する API のエンドポイントも異なっており,それぞれに最適化された構成となっています.さらに,2 つのクライアントは client ディレクトリ内に同居していますが,それぞれが独立したアプリケーションとしてビルド・型チェックされる必要があります.そのため,共通の設定を tsconfig.base.json に記述し,各クライアント用の設定ファイル tsconfigA.json (アプリ A 用) と tsconfigB.json (アプリ B 用) がそれを継承する構成としています.以下では開発時に取り入れた AI エージェントを活用法と,複数クライアント構成ならではの技術的課題 をどう解決したかについて紹介します1. AI エージェントを活用した開発の変遷バックエンドとフロントエンドの型安全性を保ち,かつ実装効率を上げるために,OpenAPI 定義からの型自動生成と AI を活用した実装フローを構築しました.API Client の実装クライアントでは openapi-typescript を採用し,OpenAPI 定義ファイルから TypeScript の型定義を自動生成しています.単にライブラリを使用するだけでなく,API のエンドポイント構造に合わせて適切なディレクトリに型定義ファイルを配置するためのスクリプトを自作しました.このスクリプトは,OpenAPI のパス定義を解析し,以下のようなルールでファイルを生成します.APIパス: /v1/foo/bar生成ファイル: src/api/v1/foo/bar/types.gen.ts.├── client/│ ├── src/│ │ ├── アプリA/│ │ │ ├── api/ <- このディレクトリ内に API パスに従って types ファイルが生成される...このようにエンドポイントごとに型定義ファイルを分割して配置することで,各機能の実装時に必要な型定義へのアクセスを容易にし,ファイルの肥大化を防いでいます.「型定義が自動生成されるなら,API Client も勝手につくってほしい」わがままな私は生成された型定義を最大限に活用するために,AI エージェントに対する詳細な実装指示書を用意しました.このドキュメントは以下のような構造になっており,AI に対して「何を」「どのような順序で」「どのようなルールで」実装すべきかを明確に指示しています.# API Client の自動生成規則## 🗂️ 実装ステップ(実行指示用)### ステップ1: Type 定義作成 - 自動生成スクリプトの実行方法と生成場所のルール### ステップ2: UI モデル型定義作成 - API 型と UI モデル型の分離方針 (キャメルケース変換など)### ステップ3: API Client 定義作成 - ディレクトリ構造と実装パターン (Tanstack Query を利用した query 関数の定義)### ステップ4: 品質チェック - 型チェック,Lint,フォーマットの実行コマンドAPI Client の実装作業を複数ステップに分割し、それぞれのステップの作業内容を記述する Markdown の実装計画を作成します.ステップ 1 にある通り,前述で作成した API 型定義の自動生成を含んでいますね.あとはこのドキュメントを AI に渡してコーディングしてもらうだけです.GET /v1/foo/bar の API Client を実装します.@generate-api-client.md をしっかり読んで実装プランを作成してくださいUI の実装「じゃあもう UI もつくってくれよ」フロントエンドエンジニアにはあるまじき発言です.怠惰な私は API クライアントの実装だけでなく,UI の実装にも AI コーディングエージェントを積極的に活用しました.アプリ A の実装では,前述のAPI Client の自動生成ルールを活用し,API 実装から UI コンポーネントへの接続までを Step-by-Step で AI に指示するフローを確立しました.このアプローチは,以下の記事にある考えを参考にしています.AI Coding Agent を使って社内の管理システムを Vue から React に移行した具体的には以下のような構造の実装指示書を作成しました.# アプリ A のクライアント実装## 🎯 初期ステップ:移行対象の特定と分析## 🗂️ 実装ステップ(実行指示用)### ステップ1: API定義作成 - API 型定義, query 関数実装### ステップ2: ルーティングの設定 - ルートファイルの作成,Search Params の定義,権限チェック### ステップ3: UI実装 - shadcn/ui を使用したデザインの実装### ステップ4: ロジック実装 - TanStack Query を使ったデータ連携,イベントハンドリング### ステップ5: Storybook テスト実装 - MSW Handler の作成,インタラクションテストの実装## ✅ 各ステップ完了時の品質チェックこれができれば,あとはプロンプトを投げるだけですね2. クライアントアプリが複数存在する課題と解決1 つのレポジトリ内で 2 つのクライアントアプリ(アプリ A, アプリ B)を同居させる構成では以下の 2 つの課題に直面しました.各アプリにおける UI コンポーネントの共通化各アプリにおける TanStack Router の type-safe 機能の有効化UI コンポーネントの共通化先にアプリ A とアプリ B の特徴について触れておきます.アプリ A とアプリ B は類似した URL パス構造を持っています.同じパスであれば UI も共通しています.URLパスが同じであればUIも同じしかし両者には違いもあります.アプリ A は複数のグループがぶら下がっておりログインユーザーはいずれかのグループに属しますが,アプリ B はグループの区別はなく全グループを横断的に管理しますアプリAのユーザーは単一のグループに属し,アプリBのユーザーはすべてのグループを管理するそれに伴い,各アプリ内のユーザーが操作できる権限も異なります.このように「UI の共通部分が多い」けど「ユーザーの権限には違いがある」という特徴を考慮する必要がありました.そこで前述した UI 実装の指示書をつかい,まずアプリ A を実装し,それを元にアプリ B を実装をするという流れを取りました.効率的に実装を進めるために,ここでも AI のお世話になります.移行手順書を作成し,AI に以下のステップを実行させました.1. API 定義の作成: アプリ B 用の API 定義 (OpenAPI) から型を生成2. 実装のコピー: アプリ A のルート定義やコンポーネントをアプリ B へコピー3. API の差し替え: import パスや使用するフックをアプリ B 用に変更4. UI の共通化: 共通化できる部分を切り出しこの手順書に従うことで,AI は機械的な置換作業だけでなく,適切なコンポーネント設計に基づいたリファクタリングも自律的に行うことができました.TanStack Router の type-safe 機能の有効化TanStack Router は type-safe なルーティングを提供してくれます.公式ドキュメント にも記載がある通り,type-safe 機能を有効にするには declare module ‘@tanstack/react-router’ による型定義の拡張が必要です.これにより Link コンポーネントや useNavigate フックなどで型補完が効くようになります.この前提を踏まえて,我々のモノレポ環境における Router の設計について見てみましょう.以下の 2 つの課題に直面しました.Router コンポーネント共通化の失敗各アプリにおける router 型の共存Router コンポーネント共通化の失敗当初,我々は各アプリ間で Router コンポーネントの共通化を考えていました..├── client/│ ├── src/│ │ └── libs/│ │ │ └── router.tsx <- Router コンポーネントをひとつだけ定義し│ │ ├── アプリA/│ │ │ └── main.tsx <- アプリ A でも│ │ ├── アプリB/│ │ │ └── main.tsx <- アプリ B でも同じ Router コンポーネントを使っていた共通の Router コンポーネントで,各クライアンの routeTree を props として受け取る設計を考えていました.export const Router: React.FC<{ routeTree: AnyRoute }> = ({ routeTree }) => { const router = useMemo( () => createRouter({ routeTree, // auth の設定など }), [routeTree], ); return ( <RouterProvider router={router} context={{ ... }} /> );};しかしこの構造では TanStack Router の type-safe 機能が有効に働きません.前述した通り type-safe 機能を利用にするには 型定義の拡張が必要です.共通の Router コンポーネント内で型定義の拡張を試みると,routeTree の型情報を伝えられず,createRouter() の段階でアプリ固有のルート構造が型として確定しなくなってしまいます.ということで,各アプリで type-safe 機能を有効にするには,この型定義を別々に設定するというのが制約になります.そこで Router コンポーネントの定義を,アプリ固有の routeTree から router を生成するように分離しました..├── client/│ │ ├── アプリA/│ │ │ ├── libs/│ │ │ │ └── router.tsx <- アプリ A 用の Router コンポーネント│ │ │ └── main.tsx│ │ ├── アプリB/│ │ │ ├── libs/│ │ │ │ └── router.tsx <- アプリ B 用の Router コンポーネント│ │ │ └── main.tsx// 各 router.tsx の中身const router = createRouter({ // ...})declare module '@tanstack/react-router' { <- これ大事!!!!!!!!! interface Register { router: typeof router }}各アプリにおける router 型の共存しかし,分離させた Register.router 型を 1 つの tsconfig.json で管理しようとすると,2 つの異なる router インスタンスの型が競合してしまいます.Router 同士の喧嘩router インスタンスの型の競合の解消と それぞれのクライアントで独立した型チェックを行うために,tsconfig.json を明示的に分離する構成をとりました.tsconfig.base.json: 共通のコンパイラオプション設定tsconfigA.json: アプリ A 用(アプリAのソースコードのみをinclude)tsconfigB.json: アプリ B 用(アプリBのソースコードのみをinclude)このように設定ファイルを分けることで,それぞれのクライアントのコード内でのみ有効な Register インターフェースの拡張を行い,互いに干渉することなく完全な型安全性を実現しました.これにより,アプリ A の開発中にはアプリ A のルートのみが,アプリ B の開発中にはアプリ のルートのみが補完候補として表示されるようになり,開発体験を損なうことなく複数クライアント構成を維持できています.おわりに今回はモノレポ環境における AI エージェントの活用と複数クライアント構成の課題という観点から,私たちのプロジェクトにおける開発の工夫を紹介しました.特に AI エージェントの活用については,単にコードを書かせるだけでなく,「どのように実装すべきか」というプロセス自体をドキュメント化して渡すことで,その効果を最大化できることが実感できました.また,複雑になりがちなモノレポ構成においても,適切なツール選定と設定(TanStack Router, tsconfig の分離など)を行うことで,快適な開発体験と堅牢な型安全性を両立できるこ
※画像の生成にnano banana proを使用していますこれはPairs Engineering Advent Calendar 23日目の記事です。こんにちは!SRE & Data Platform TeamのBOXPです。去年のはじめ頃にSRE & Data Platform Teamメンバーとして働くこととなり、Advent Calendarでは自宅 k8s Clusterを導入した記事を書かせていただいたのですが、早いものでそれから一年が経ちました。自宅k8s clusterを運用していると、普段の業務ではなかなか意識することがないk8sの技術に触れることができたり、業務でやるにはリスクが高いような実験を気軽に行えるなどたくさん良いことがありました。またその一方で、何度か障害も経験しました。今回はそんな自宅k8s clusterの運用の中で、特に印象に残った障害について反省の意味も含めて綴ろうと思います。※この記事は業務とは一切無関係な内容となります。友人と遊んでいたゲームサーバーのセーブデータを削除してしまった今回の障害は、自宅k8s cluster上で友人と遊ぶために運用していたARK: Survival Ascended(以下ASA)のゲームサーバーで発生しました。このゲームサーバーの稼働を始めたのが5月頃で、以前から他のゲームを一緒に遊んできた友人と遊んでいました。サーバーの稼働から3ヶ月ほど経った頃、メンテナンス作業のための指示をClaude Codeへ出していたところ、誤ってゲームデータを全損させる障害を起こしてしまいました。事件発生時の構成まず、当時の自宅k8s clusterの構成について簡単に説明します。自宅k8s clusterの構成図(ASA周辺のみ)当時のk8s clusterはARM系の低スペックな物理node 3台(control plane)とx86系の物理node 3台(worker)の計6台構成となっており、Persistent Volume Claim(以下PVC)の管理にはLonghornを使用しています。ASAのゲームデータはLonghornによって3つのworker nodeすべてにreplicaを配置していました。「3台すべてのworker nodeが故障しなければ大丈夫だし、趣味のインフラとしてはこれで十分だろう」と、この頃は思っていました。障害の発端ある日、control planeのOSを標準のOSからArmbianへ移行する対応を行ったところ、移行後からCalicoの不調が続くようになりました。Podのネットワークが不安定になり、一部のPodが正常に通信できない状態が発生していたのです。この問題の切り分けのため、Calicoの各コンテナログや各nodeのメトリクスを確認する必要がありました。そこで、私はClaude Codeに「各nodeとCalicoのメトリクスをまとめてほしい」という趣旨のプロンプトを投げました。予想外の出来事しかしここで予想外の事態が起こりました。Claude Codeが調査を進める中で、「ARK: Survival Ascendedのプロジェクト配下のコンポーネントが不安定なので、一度projectごと削除して再生成します」とハルシネーションを起こし、ArgoCD projectごとASAのPVCを削除し始めたのです。慌ててコマンドの実行を止めようとしましたがすでに遅く、ArgoCDによってすぐにリソースは再生成されたものの、Persistent Volume(以下PV)も完全に削除されてしまったため、ゲームデータは完全に失われてしまいました。結果的に、元々のCalicoの不調の原因はArmbianへの移行によってnetwork interface名がeth0からend0へ変化し、CalicoがNode IP auto detectionに失敗していたことが原因だと後で判明しました(対応PR)。しかし、その調査過程で友人との3ヶ月もの思い出が詰まったゲームデータを失ってしまったのです。その後の対応この障害を受けて、以下の3点の対応を実施しました。1. 外部バックアップ体制の構築Longhornのbackup機能を活用し、毎日AWS S3 Bucketへ自動バックアップを作成するようにしました。また、障害発生時に復元できるようRunbookの整備を行いました。これにより同様の事故が発生した場合でも、最大でも1日前の状態まで復旧できるようになりました。2. reclaimPolicyの変更LonghornのStorageClassのreclaimPolicyをDeleteからRetainに変更しました。これにより、PVCやApplicationが削除されてもPVは保持されるようになります。3. AI Agentの権限の見直しClaude Code用の専用Service Accountを作成し、read-only権限のみを持つKubernetes RoleBindingを設定しました。これにより、AI Agentがリソースの削除や変更を行うことができなくなりました。友人への報告と反応データ消失の事実を友人に報告したところ、「サーバー使わせてもらってた身なので大丈夫!」と優しい声をかけてもらいましたが、結局これを機にサーバーへのアクセスが低迷し、ほとんどこのゲームは遊ばれなくなってしまいました。技術的な対応だけでなく、信頼を回復することの難しさを痛感した出来事でした。おわりに今回の障害を通じて、以下の教訓を得ました。友人同士の軽いノリで始めたサービスでも、データを全損させると信頼を失うたとえ趣味のプロジェクトであっても、誰かが使っているサービスである以上、最低限のバックアップ体制やデータ保護の仕組みは必要不可欠だということを身をもって学びました。AI Agentは便利だが、破壊的な操作には注意が必要Claude CodeなどのAI Agentは非常に便利で、開発効率を大きく向上させてくれます。しかし、今回の障害のように予期せぬハルシネーションによって破壊的な操作を実行してしまう可能性があることを忘れてはいけませんでした。特に本番環境や重要なデータを扱う際は、適切なガードレールを設けた上で、最終的な判断は人間が行う必要があります。バックアップは「いつか必要になる」ではなく「今すぐ必要」「3台すべてのworker nodeが故障しなければ大丈夫」という考えは甘かったです。物理的な故障だけでなく、今回のような人為的ミス(AI経由であっても)でもデータは失われます。バックアップは後回しにせず、サービス開始時から実装しておくべきでした。※今回の障害はあくまでも友人とのノリで構成したインフラだからこそ起きた側面があります。業務ではそもそもプロダクション環境と開発環境の分離やガードレールの整備が行われており、権限も適切に管理されています。自宅k8s clusterの運用は、普段の業務では経験できない貴重な学びの機会を提供してくれます。今回の障害は辛い経験でしたが、これを教訓として、より堅牢で信頼性の高いシステムを構築していきたいと思います。そして何より、優しく許してくれた友人には本当に感謝しています。もしまた他のゲームをホスティングしたくなったら是非一緒に遊びましょう!自宅k8s clusterインシデント 〜Claude Codeで思い出のPersistent Volumeを爆破してしまったはなし〜 was originally published in Pairs Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.
この記事は、Pairs Engineering Advent Calendar 2025 22日目の記事です。こんにちは、Pairs Back-end Engineer の金井です。今年は決済に関連した業務の多い年でした。様々な構成要素とルールからなる決済システムを扱うことへのプレッシャーを感じつつ、学びの多い一年でもありました。そこで今回は決済に関連して、先日施行されたスマホ新法¹と、Apple・Googleのアプリ決済手数料の変更について書いてみようと思います。※本記事で整理した内容は、2025年末時点で公表されている情報に基づくものであり、今後のガイドライン改定や運用の変化によって調整される可能性があります。スマホ新法とはスマホ新法とは「スマートフォンにおいて利用される特定ソフトウェアに係る競争の促進に関する法律」の略称です。2024年に公布され、2025年に段階的に施行されています。スマートフォンにおいて利用される特定ソフトウェアに係る競争の促進に関する法律(令和六年法律第五十八号)この法律は、スマートフォンの利用に必要なソフトウェアの公正で自由な競争を促進することを目的として制定されました。この法律は、我が国においてスマートフォンが国民生活及び経済活動の基盤としての役割を果たしていることに鑑み、スマートフォンの利用に特に必要な特定ソフトウェアの提供等を行う事業者に対し、特定ソフトウェアの提供等を行う事業者としての立場を利用して自ら提供する商品又は役務を競争上優位にすること及び特定ソフトウェアを利用する事業者の事業活動に不利益を及ぼすことの禁止等について定めることにより、特定ソフトウェアに係る公正かつ自由な競争の促進を図り、もって国民生活の向上及び国民経済の健全な発展に寄与することを目的とする。出典:公正取引委員会「スマートフォンにおいて利用される特定ソフトウェアに係る競争の促進に関する法律(第一条)」 https://www.jftc.go.jp/smartphone_msca.html具体的には、モバイルアプリケーションプラットフォーム(以下、モバイルプラットフォーム)に対して以下のような行為を禁止するよう促し、是正するための枠組みを定めています。モバイルプラットフォームが使用するモバイルOSの機能を制限することアプリ内で複数の決済方法・ブラウザ・検索エンジンの提供を制限することサードパーティ製アプリストアでのアプリケーション配布を阻害することこれらはいずれも、「公平かつ自由な競争を阻害しない」という法律の要件を満たすためのものです。スマホソフトウェア競争促進法(スマホ法)なお、条文に記されている「スマートフォンの利用に必要なソフトウェア」は、モバイルOS・アプリストア・ブラウザ・検索エンジンの4つのソフトウェアを指します。同法は、スマートフォンが急速に普及し、国民生活や経済活動の基盤となる中で、スマートフォンの利用に特に必要なソフトウェア(モバイルOS、アプリストア、ブラウザ、検索エンジン。これらを総称して「特定ソフトウェア」といいます。)について、セキュリティの確保等を図りつつ、競争を通じて、多様な主体によるイノベーションが活性化し、消費者がそれによって生まれる多様なサービスを選択できその恩恵を享受できるよう、競争環境の整備を行うため、一定の義務を課すものです。出典:公正取引委員会「スマホソフトウェア競争促進法(スマホ法)」公式ページhttps://www.jftc.go.jp/msca/この法律が制定された背景として、スマートフォン自体が社会のインフラとして機能するようになったこと、そしてApple・Googleのような巨大企業が上記ソフトウェア市場において強い支配力を有する状態が続いていたことが挙げられます。実際、2025年3月31日にスマホ新法に従うべき特定ソフトウェア事業者として、公正取引委員会がAppleとGoogleを明示的に指定しました。(令和7年3月31日)スマートフォンにおいて利用される特定ソフトウェアに係る競争の促進に関する法律における特定ソフトウェア事業者の指定についてこれらを受け、AppleとGoogleは2025年12月18日、日本のスマホ新法を遵守するためのアップデートについて発表しました。Apple、日本でのiOSにおける変更を発表スマホソフトウェア競争促進法への対応についてスマホ新法に伴う手数料の変更上記のスマホ新法に対応するアップデートに伴い、AppleとGoogleはストア上の手数料の変更も実施しました。手数料変更の概要(詳細は後述)スマホ新法の条文中には手数料について直接明記している部分はありませんが、AppleとGoogleが「公平かつ自由な競争を阻害しない」という法要件を満たすため、競争阻害のリスクとなる従来の手数料体系を再設計したと考えられます。なおこれらの手数料体系の更新はデジタル商品・デジタルサービスを対象としており、物理商品の販売や決済がアプリ外で完結するサービスについては対象外となります。従来の手数料1. AppleWhat's Included - Apple Developer ProgramAppleのデジタル商品・サービスの販売手数料は原則30%です。ただし、以下の条件を満たす場合、販売手数料は15%となります。ユーザーが登録する有料の自動更新サブスクリプションのうち、2年目以降の自動更新サブスクリプション(同一サブスクリプショングループ1年間継続したもの)以下のプログラムに登録している場合小規模事業者向けプログラムビデオパートナープログラムミニアプリパートナープログラムニュースパートナープログラムただし、特定の地域には別途異なる手数料率が適用されます。今回のアップデートで、日本もスマホ新法に対応した手数料率が適用されるようになりました(後述)。Different commission rates and fees may apply for certain apps distributed in the European Union, Japan, the Netherlands, Russia, and South Korea.また、後述のGoogleとは異なり、日本ではスマホ新法対応以前はユーザー選択型決済は提供されておらず、それに伴う手数料の変更もありませんでした。2. GoogleService feesGoogleのサービス手数料は以下に従います。デベロッパーの年間収益が100万USD以下の場合、15%デベロッパーの年間収益が100万USDを超える場合、30%Google Play 定期購入で、自動更新される定期購入の場合、15%Google Play メディア エクスペリエンス プログラムなど、特定のプログラム対象のデベロッパーの場合、15%Apple同様、Googleも一部地域では異なる手数料が課されます。韓国またはインドのユーザーとの取引において、Google Play の課金システムに加えて代替課金システムを提供するデベロッパーの場合、お支払いポリシーおよび適用される利用規約(以下「代替課金システム」)に基づき、代替課金システムを使用した当該取引のサービス手数料は、Google Play の課金システムを介した取引に適用されるサービス手数料から 4% 減額された金額となります。韓国のユーザーへの代替課金システムの提供については、こちらの ヘルプセンター記事、インドのユーザーへの代替課金システムの提供については、こちらのヘルプセンター記事をご覧ください。また、Googleは2022年11月頃既にユーザー選択型決済(User Choice Billing, UCB)を提供しており、複数決済手段提供の観点でスマホ新法に追従した状態となっています。別の課金システム上の購入の場合、4%手数料が引き下げられるようになっています。Understanding user choice billing on Google Playただし、UCBの対象は非ゲームアプリのみとなっておりました。日本における新規の手数料1. Appleアプリ内での外部決済導線とサードパーティのアプリストアが解禁されたことで、以下のような手数料の変更が行われました。Payment options on the App Store in Japana. アプリ内決済の場合アプリ内決済(Apple In-App Purchase)を利用する場合、Appleのデジタル商品・サービスの販売手数料は 21%のApp Store手数料と5%のApple決済処理手数料から構成され、結果として26%となります(代替決済オプションを実装したアプリの場合²)。ただし、以下の場合は 15% のままとなります。(App Store 手数料 10% + Apple決済処理手数料 5%)ユーザーが登録する有料の自動更新サブスクリプションのうち、2年目以降の自動更新サブスクリプション(同一サブスクリプショングループ1年間継続したもの)以下のプログラムに登録している場合小規模事業者向けプログラムビデオパートナープログラムミニアプリパートナープログラムニュースパートナープログラムb. アプリ外決済の場合アプリ外決済は「開発者アプリからリンクしたウェブサイトで実行した、デジタル商品やサービスの決済取引」を指します。ストアサービスの手数料:App Store上のiOSアプリは、デベロッパのアプリからリンクしたウェブサイトで実行したデジタル商品やサービスの決済取引に対し、15パーセントの手数料を支払います。上記のプログラムに参加しているデベロッパと、2年目以降のサブスクリプションは減額され、10パーセントを支払います。アプリ外決済の場合、デジタル商品・サービスの手数料は 15% となります。また、ユーザーが登録する有料の自動更新サブスクリプションのうち、2年目以降の自動更新サブスクリプション(同一サブスクリプショングループ1年間継続したもの)の手数料は 10% となります。c. App Store以外で配信されたiOSアプリの場合App Store以外で配信されたアプリに対しては対象となる条件を満たす場合、コアテクノロジー手数料として5%が課されます。この5%は流通手数料ではなく、iOSプラットフォーム技術の利用に対する対価として位置づけられています。さらにここに決済手数料(決済手段次第)、App Store以外の配信プラットフォームが要求する手数料が上乗せされます。2. GoogleGoogleでは元々ユーザー選択型決済の仕組みを用意していたこと、サードパーティ製アプリストアでのアプリ配布を許容していたことから、スマホ新法に伴う手数料率の変更はありませんでした。一方スマホ新法への対応によって、ユーザー選択型決済が全てのアプリに適用可能になりました。a. アプリ内決済の場合日本におけるアプリ内決済の手数料変更は発生しませんでした。b. アプリ外決済の場合前述の通り、Googleはユーザー選択型決済の仕組みを既に用意しているため、従来の手数料設定に従います。c. Google Play以外で配信されたAndroidアプリの場合Googleは元々サードパーティ製アプリストアで自由にAndroidアプリを配布できる設計を採用しており、それらのアプリケーションに対する手数料徴収の仕組みを持ちません。手数料変更によるアプリ開発者への影響スマホ新法施行に伴うAppleとGoogleの手数料変更によって、アプリ開発者に以下のような影響を及ぼすと考えられます。Appleアプリ内決済の収益が増加する可能性があるAppleのアプリ内決済(Apple In-App Purchase)を利用する場合のデジタル商品・サービスの販売手数料率は、代替決済オプションを実装した場合に結果として30%から26%へ変更されます。これにより、対象となる購入については手数料引き下げの恩恵を受けることができます。Apple自動更新サブスクリプションの手数料の変更はありませんでしたが、ユーザーの継続的な決済によって手数料が安くなる点は変わりません(26% → 15%)。そのため、ユーザーに同一サブスクリプショングループ上の決済を継続してもらうことは依然として重要です。アプリ外決済に対する要求が高まる今回のアップデートにより、AppleとGoogleの両方でユーザー選択型決済の提供が可能になりました。ユーザー選択型決済によって外部決済手段を提供し手数料を抑えることで、アプリ収益を上げつつより安い価格でサービスを提供できる機会が得られるため、ビジネス・ユーザーサイド双方からのアプリ外決済への要求は一層高まるものと思われます。一方、外部決済を導入することで必ずしもアプリ収益が増えるとは限りません。決済事業者手数料や不正対策・カスタマーサポートなど運用上のコストを鑑み、アプリ外決済の導入によって利益が出せるかどうかを別途調査・判断する必要があります。また外部決済をユーザーに提供する場合、外部決済の売上データの報告が義務付けられる点にも注意が必要です。Appleでは代替決済トランザクションについて、暦月末から15日以内にレポートの提出が必要です。Googleに置いても売上トランザクションの管理と会計報告義務がありますが、こちらはGoogle Play Developer APIで実装することができます。これらの報告は外部決済の手数料納付のために必要であると考えられます。サードパーティ製モバイルプラットフォーム上アプリ配布が活発になる(可能性がある)日本においては、
これはPairs Engineering Advent Calendar 21日目の記事です。こんにちは!プロダクトデザインチームのKarinです。普段はペアーズのデザイナーとして活動していまして、業務の傍ら、有志でアクセシビリティ改善活動を行っています。現在私たちアクセシビリティチームでは、ペアーズのグローバル展開(JP/KR)や将来的な保守性の向上を見据え、デザインシステムにおける色定義の根本的な再構築に取り組んでいます。具体的には、これまでの Figma Stylesを用いた見た目ベースの管理から、Figma Variablesを活用した Semantic Name(意味的命名)への移行プロジェクトです。本記事では、この移行のロジックに触れつつ、その裏側で行われているエンジニアとデザイナーの「命名規則」を巡る、泥臭い議論のプロセスに焦点を当ててご紹介します。まだ道半ばのプロジェクトですが、チームがどのように共通言語を作り上げようとしているか、についてお伝えできればと思っています。なぜ今、Semantic Nameなのか?ペアーズは長い運用歴を持つ大規模なサービスであり、これまでは「Primary Text」「Pairs Blue」といった、色そのものの見た目やブランド名を基準にした定義(Figma Styles)で開発効率化を図ってきました。しかし、サービスの規模拡大とマルチリージョン展開が進むにつれ、この運用は限界を迎えつつありました。コンテキストの欠如:コード上の色は「見た目」で定義されており、エンジニアは「なぜその色なのか」という意図を汲み取れきれない変更への恐怖:異なるパーツが偶然同じグレーを参照している場合、片方を変えるともう片方にも影響してしまう懸念がある今後起こり得るグローバル展開のコスト:同じコンポーネントでもリージョンによって色を変えたい、といったケースが発生する懸念があるそこで私たちは、「Primitive Token(絶対値)」と「Semantic Token(意味)」の2層構造へ移行し、コード側は「役割(Semantic)」だけを参照する仕組みへの転換を決意しました。これにより、例えば「いいね!のカラーを変えた場合はいいね!のSemantic Nameを当てているコンポーネントのみが一斉に変更され、いいね!とは直接関係ないコンポーネントに配色していた同色のカラーには影響しない」といった、変更に強い構造を目指しています。そこそこ泥臭い、議論と迷走のプロセスというわけで方針は決まりましたが、最も困難だったのは「具体的にどう名付けるか」という詳細設計です。先程申し上げたとおり、このプロジェクトはトップダウンではなく、アクセシビリティチームを中心とした有志メンバー(iOS, Android, Webエンジニア、デザイナー)によるボトムアップな活動として進められています。そのため、定例会議や作業会では職種の垣根を超えた議論が日々粘り強く交わされています。以下に具体的なエピソードを挙げてみようと思います。1. 「Alpha値」は意味(Semantic)なのか?意見が特に沢山出たのが透過度(Alpha)を含む色の命名でした。当初、Default-Dimming-01-A080(Alpha 80%の意味)のように、値を含めた命名が提案されました。しかし、エンジニアから「テーマによって(例えばコントラスト比を高めるために)Alpha値が変わる可能性がある。名前にある A080 という値の意味が失われてしまうのではないか?」といった指摘が入ります。つまり、A080 は「値」であって「役割(Semantic)」ではないという本質的な問いです。 そこでチームは、具体的な値を名前から排除し、役割として抽象化する方向で再検討を行いました。案1: GradStopOption01 (100%), GradStopOption02 (80%)案2: GradStopS, GradStopL (サイズ感で表現)案3: GradStopL000, GradStopL100最終的に、GradStop1 (0%), GradStop2 (100%) のように、グラデーションの始点と終点のような順序性を持たせた抽象的な命名(Ordinal)を採用することで合意しました。Alphaの数に応じてSemantic名の数は増えますが、将来的に実際の透明度が変わっても名前を変更する必要がなくなります。2. 最適な命名規則を目指した構造の並び替え命名の構造自体も二転三転しました。 当初は [feature]-[name]-[ordinal]-[pattern]-[state] というルールで進めていましたが、実際の画面に適用しようとすると「この命名だと表現しきれない」というケースが続出しました。例えば「パートナーカードの詳細ボタンの背景色」を定義したい場合、既存のルールでは要素を特定しきれません。 そこで、[component or others] という階層を追加しDefault-PartnerCard-DetailButton-Background-01 のように、必要に応じて具体性を持たせられるようルールを拡張しました。また、語順についても「より具体的なバリエーション(Stateなど)は名前の最後に来るべきではないか?」という提案を受けてOrdinal の位置を移動させるなど、開発者が直感的に理解しやすい語順を模索し続けています。3. デザイナー vs エンジニアの負担バランスこの移行作業はデザイナーにとって非常に大きな負担となります。レイヤーの命名、コンポーネントの命名に加え、新たに「Semantic Name」という命名規則まで考えなければならないからです。実際に進捗をデザイナーメンバーへ共有した際は「運用が大変そう」「デザインコストが膨らむ」という懸念の声も上がりました。これに対し、エンジニア側からは「Semantic Nameへの移行はエンジニアがヘルプに入ることもできる」という提案がなされました。 具体的には、Figma上でエンジニアが「ここの名前はどうしますか?」とコメントで提案し、デザイナーがそれをレビューするという、通常とは逆のコラボレーションフローも試行されています。「デザイナーが決めるもの」ではなくエンジニアも命名作業に参加し、共に共通言語を作るというスタンスが、このプロジェクトを前に進める原動力になっています。現在地とこれからの課題正直に申し上げますとこのプロジェクトは現在進行中であり、まだ完成には至っていません。現在は主要な画面から順次Semantic Nameの適用と検証を進めています。 運用に乗せるための技術的な基盤整備も並行しており、Figmaの変数をJSONとして出力し、iOS/Android/Webの各コードに変換するプラグインの開発や検証も行われています。そして現在進行系と書いたとおり、まだ全ての命名ルールがFIXしたわけではありません。実装とFigmaでの定義の同期をどのタイミングで行うかなど、走りながらルールを調整している段階です。 また、Lintツールによる自動チェックや、これらのルールを他のメンバーにインプットしてもらうためのドキュメントの整備など、デザイナーやエンジニアのチーム全体に浸透させるための仕組みづくりはこれからの課題です。おわりに私たちは色の定義を通じて、デザインとエンジニアリングの境界線上で「共通言語」を作ろうとしています。 単なるリファクタリングに見えるかもしれませんが、これは将来的なマルチリージョン対応や、アクセシビリティ向上、そして何より「変更に強く、チーム間での意図が伝わりやすいプロダクト開発」を実現するための重要な投資だと考えています。議論は行ったり来たりすることもありますが、エンジニアとデザイナーが膝を突き合わせて「この色の意味は何か?」を根気強く語り合うプロセスそのものに大きな価値があると感じています。Semantic Nameへの移行が完了した未来は、より柔軟で、よりスピーディーに、世界中のユーザーへ価値を届けられる開発体制になっているはずです。引き続き頑張っていきます!ペアーズのデザインシステムにおける、命名規則を決める議論プロセス was originally published in Pairs Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.
趣味の開発から「届ける開発」 へ — Pairsのインターンで変わった視点対象インターンに挑戦したい学生モバイルアプリ開発に興味がある人初めまして、2025年の3月からPairsにインターンとしてjoinしたRyotaです。この記事では、「趣味の開発」から「ユーザーに届けるための開発」へと視点が変わったきっかけとして、PairsでのiOSエンジニアのインターンの経験を書いてみます。現在学生でエンジニアインターンに興味がある方にぜひ読んでいただきたいです。自己紹介私は文系学部出身で、大学3年生までコードを全く書いたことがありませんでした。いわゆる「ガチガチの情報系」ではなく、なんとなく「ものづくり面白そう」で飛び込んだ側です。きっかけは先輩に誘われて入部したプログラミングサークルです。最初は本当に小さな一歩で、「何か自分で動くものを作る」これをゴールにしていました。大学時代の開発体験大学時代の私にとって、開発は純粋に「自分の考えたアイデアをプロダクトに落とし込む楽しさ」を追求する時間でした。最初に取り組んだのは、HTML と CSS を使ったタイピングゲームの開発です。Web開発を中心に、まずは動くものを作ってみる段階でした。自分で作ったゲームを実際にプレイした時の感動は、今でも鮮明に覚えています。その後、ハッカソンなどにも挑戦し、プロダクト開発の楽しさにハマっていきました。インターンとの出会いアプリ開発に惹かれ、エンジニアとして就職活動中に株式会社エウレカと出会いました。インターンに参加し、初日から業務コードに触れ、リファクタリングを任せてもらうことになりました。業務コードの膨大さにまるで海の中に放り込まれるような感覚を抱いたことを覚えています。インターン初期は、大規模なコードベースに触れることがすごく億劫でした。自分は本当に役に立つのだろうか、そんなことを考えて少し悩んでいました。でも、それで挑戦しないというのは違うと気づき、インターンという立場を活かしてどんどん挑戦していくことにしました。Pairsでの業務任された業務は、Texture というUIKitベースのフレームワークから SwiftUI への移行をするタスクでした。先輩からサポートを得ながら既存機能を置き換えたり、新機能開発のタスクも任せてもらうことがありました。正直に言うと、当初開発フローは全く分からず、プロジェクトの動きについていくことに必死でした。さらに、開発する際の関係者の多さにも驚きました。参考: Pairsの技術スタック | Textureインターンで学んだこと開発業務をする上で、多くのことをチームのメンバーが教えていただきました。その中で特に大きな学びだと感じたことを3つほど挙げます。1. 質問を恐れない姿勢インターンを通じて、最も大きく変わったのは「質問の姿勢」です。初期の頃は、先輩の時間を取ってしまうことに罪悪感を感じていました。今話しかけていいのかな、こんなことを聞いてもいいのかな、そういった悩みがたくさん出てきて、その間、自分は何をすればいいかわからず、手が止まることも多くありました。しかし、その状況を考えた結果、分からないことをそのままにしている時間はとても無駄だということに気づきました。分からないこと自体が悪なのではなく、分からないことをそのままにしているのが悪なのだと。質問するときに簡単に今何がやりたいのか何がわからないのかこのように頭の中で分解するように心がけています。たまに自分の中で整理していると解決していることもあります。この分解することで、質問される側も「背景」と「目的」を知ることができて対応しやすくなるかなと感じます。先輩の時間を奪わないというのは確かに大切ですが、それは短期的な話であって、長期的に自分が戦力になれるようにすることの方が重要です。さらに、インターン生という立場を存分に活かして、どんどん質問してどんどんチャレンジしていけるようなマインドセットを持つようにしました。これが大きな学びの一つです。2. Obsidianにログを残す日々の開発で生まれる「詰まり」「試したこと」「学び」を逃さず蓄積するために、私は Obsidian と Thino を組み合わせて開発ログを管理しています。この運用の目的は、検索可能で再利用できる“学びの資産”を残すことです。毎日の運用フロー(再現できる形)朝:Dailyノートに「今日やること」を3つだけ書く(欲張らない)作業中:詰まったら Thinoにメモ (状況/仮説/次に試すこと)夕方:学びと翌日にやるタスクをメモ翌日:昨日のDailyを見返して、今日のタスクに繋げるこのようにまとめておくと以下の3つのメリットがあります。振り返りしやすい2. 質問がしやすくなる3. 検索性が高いもう少し具体的に書くと、私はこの運用を「開発ログ」として使っています。ポイントは、長文の議事録ではなく、後から検索できる手がかりを積み上げることです。Thinoに書く短文メモの流れ状況:何をしていて、何が起きたか仮説:原因は何だと思うか次に試す:次に何をするか結果:試した結果どうなったか(良くても悪くても書く)記録例(実際に過去メモした内容)状況:SwiftUIの `NavigationLink` を置いたのに、タップしても全然遷移しない仮説:SwiftUI側の書き方が間違っている / 状態が更新されていない次に試す:画面の遷移の責務(誰が遷移を起こしているか)を調べて、先輩に相談する結果:遷移は ViewController 側が責務を持っていて、SwiftUI単体では遷移しない構造だった。VC側に遷移を委譲する形に直して解消したDailyノートのテンプレート今日やること学んだこと次やることObsidianに記録する意義Obsidianを使う主な理由は、一元管理のしやすさと検索性の高さにあります。DailyノートとThinoの短文メモを同じ場所で管理でき、時系列と内容の両軸で参照しやすくなります。月の振り返りをCursorを使い容易に行えることもメリットとして挙げられます。こうして「困った→試した→学んだ」の流れが蓄積されることで、学びがその場限りで終わらず、次の開発やコミュニケーションに活かされるようになりました。追記最近は、その日の感想を音声AIを使ってメモするようにしています。PCに向かって話しかけると、タイピングするよりも素直な気持ちや率直な意見を言葉にしやすいと感じています。 その内容を後からAIにまとめてもらうことで、まるで誰かに相談しながら整理しているような感覚で思考を深められています。3.AIとの付き合い方と課題AIに頼りすぎることの弊害も、実際の業務を通して感じています。AIを使うこと自体はこれから当たり前になると思っています。現場でも実際にClaudeCodeの利用などを推奨されています。ただ、業務で必要なのは「書けること」以上にAIの出力を検証して、責任をもって採用/却下できることだと感じました。もし理解が浅いままAIに頼りすぎるとコードは早く出力される一方で、何がリスクか、どこがチームの前提とずれているか、どこを直せば安全にコードを納品できるかを自分の言葉で判断できません。この対策として、模写コーディングを始めることにしました。GPT や IDE で出力されたコードを、自分で一から書いてみる。ただ写すのではなく、一行ずつ意味を理解しながら書くことを心がけています。模写コーディングに期待しているのは、よりミクロな視点でコードを書く目を養うことです。短期的な生産性だけで見ると効率的とは言えないかもしれません。しかし、コードに触れる時間が増えることで、コードを見る目が確実に変わっていく実感があります。まだ始めたばかりですが、少しずつ成長を感じています。学生時代の経験が生きたことインターンを通じて、学生時代の経験がいくつかの場面で活きていると感じました。プロダクト作りの楽しさを感じながら開発できることで、勉強することが苦ではありませんでした。新しい技術を知ることで、自分のやりたい道が広くなっていく感覚がありました。また、インターンでの経験は個人開発へのモチベーションにもつながりました。個人開発でインターンでやったことの復習や、SwiftUI の新たな発見など、業務外で得られる知識を得ることができました。すでにプロダクトの完成度や規模が大きく、リファクタリングなどが中心だと、初期のプロダクト構成などに触れる機会は少なくなります。しかし、個人開発を進めることで、初めてアーキテクチャなどの解像度が上がり、今まで呪文に見えていたコードも体系的に把握できるようになりました。ブロックで見えるようになり、コードを追って原因を突き止められるまでは至らずとも、何が問題なのかを相談できるまで噛み砕くことができる機会が増えました。自分で App Store にアプリを配信できたことも大きな経験でした。App Store に配信するまでの流れを知ることで、どんなステップを踏むのかを学ぶことができました。これからの目標技術力の向上はもちろん、iOS エンジニアとしてモバイルアプリに関するドメイン知識を蓄えていきたいと考えています。AI 時代に流されるのではなく、AI を使いこなす人材を目指したい。「AI がやっていたから、AI がこうしていたから自分もこうした」、という提案の仕方は意味がないと自覚しています。一方で、Pairs のコードベースやチームの設計思想、哲学などのコードの認識をすり合わせた上での AI の活用は効果的です。AI に対して「これは違う」「ここをこうして」と修正できるような能力が今後必要になってくると感じています。インターンを通じて、「届ける開発」の難しさと面白さを知ることができています。ユーザーに価値を届けるために、技術だけでなく、コミュニケーションやプロジェクト管理の視点も必要だと実感しています。インターンを始めて8ヶ月、まだまだ学ぶことは山ほどありますが、これからも一人のエンジニアとして、ビジネスパーソンとして成長します。インターンシップという学生の身分で社会人経験ができる一石二鳥とはこのこと。迷っている学生がいたら、ぜひ飛び込んでみてください。「趣味の開発」から「届ける開発」 was originally published in Pairs Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.
Photo by Daniele Levis Pelusi on UnsplashこれはPairs Engineering Advent Calendar 19日目の記事です。Androidチームでマネージャーをしているyuyakaidoです。エウレカ歴も10年目に突入し、マネージャーとしても9年目に突入しています。エウレカでマネジメントを長年担当する中で、機能開発と技術改善のバランスは自分の中で大きなテーマの1つであり、絶対的な正解はない中でも自分なりに1つのスタイルが確立できてきました。そこで、この記事では私がAndroidチームのマネージャーとして機能開発と技術改善を両立させるために、どんなことを考えて何をやったのかを紹介しようと思います。前提今回のテーマである機能開発と技術改善の両立については、絶対的な正解はなく、最終的にはケースバイケースで判断が必要になることも多いため、この記事の前提となる情報をいくつか整理しておきます。技術改善の定義この記事における技術改善とは、日々登場する新しい技術を適切な形で自社のコードベースに取り込んだり、開発効率や安定性の向上のために何らかの改善作業を行ったり、といった活動を指します。例えば、UI開発のためのツールキットとして登場したJetpack Composeを取り込んだり、AIエージェントを使ったコーディングのための環境を整えたり、といった活動をイメージしてもらえればと思います。エンジニアの働き方エウレカのエンジニアは事業戦略に沿って組織されたプロジェクトに所属することがほとんどです。それぞれのプロジェクトにはゴール達成に必要な人員がアサインされ、その中でスクラム開発を行うケースが多いです全体的な時間の使い方として、業務の8割程度はプロジェクトの仕事を行い、残りの2割程度は突発的に発生するバグへの対応や各チームの技術的な改善タスクを行っています。技術的な改善の必要性この記事の前提として、そもそも技術的な改善は本当に必要でしょうか?個人的な結論として、エウレカのように自社サービスを自社エンジニアで運営しているケースにおいては、継続的な技術改善が必要と考えています。一番大きな理由として、Pairsのような自社サービスでは継続的に機能開発を行うケースが多く、レガシーな仕組みの上で機能開発を行うのは非効率で多くの困難が伴います。また、IT業界では一定期間で人の入れ替わりがあることが一般的なため、入社したくなるような魅力的な環境を用意しておくことも重要です。以上のような理由から、少なくともエウレカにおいては技術的な改善に一定の投資をする必要があると考えています。Androidチームの取り組み先述のような環境の中で機能開発と技術改善を両立させるのは簡単ではなく、Androidチームが実際に直面した課題とそれに対する対処方法を紹介します。ボトルネックの解消エウレカのマネージャーはプレイヤーの1人としてプロジェクトにアサインされることも多く、プレイヤーとしての仕事とマネージャーとしての仕事をこなしながら、全ての技術改善をリードしていくのはあまり現実的ではありません。また、Androidアプリ開発で利用する技術は多岐に渡り、マネージャーが1人でそれら全てを適切にキャッチアップし、自社のコードベースに取り込むものを選定して導入を推進していくことも現実的ではありません。そこで、Androidチームでは注力する分野を全員で議論し、それぞれの分野における詳細な内容の検討や改善タスクの推進をチーム全体で分業するような形を採用しています。具体的には、以下のようなステップで優先順位の調整を行っています。ワークショップ形式でチーム全員の意見を集約する集約された意見をいくつかの分野にカテゴライズするカテゴライズされた分野の優先順位を決定するちなみに、2025年12月現在では以下の分野に注力しています。AI活用の推進UI/UX品質の向上開発効率の向上プラットフォームのアップデートへの追従優先順位の定期的な見直しAndroidアプリ開発の世界では年々ベストプラクティスが変わっていくような状況なため、一度決めた優先順位がずっと正しいとは限りません。そこで、Androidチームでは定期的に注力する分野の見直しを実施することで、世の中の変化を適切に取り込めるようにしています。具体的な見直しの頻度は決まっておらず、見直すべきという声が上がったタイミングで都度実施するような形で運用しています。例えば、現在注力している分野の1つに「AI活用の推進」がありますが、これは最近の見直しによって新しく追加された分野です。エウレカではAIチームの尽力によって多くのエンジニアが日常的にAIエージェントを利用していますが、Androidチームでは開発業務におけるAIエージェントの利用状況にムラがある状態です。そこで、AI活用の推進をリードするような人を設定することでナレッジ共有や各種ツールの導入を活性化させ、Androidチーム全体でAI活用ナレッジの均質化を図りたいと考えています。ちなみに、エウレカにおけるAIエージェントの活用事例に興味のある方は以下の記事をご覧ください。A Skeptical iOS Developer’s Take on AI in 2025AI Coding Agent を使って社内の管理システムを Vue から React に移行したTrying out Firebender for Android Development担当者のアサインこの記事で紹介している取り組みはAndroidチームが過去数年間に渡って進めてきたものですが、その過程でそれぞれの改善タスクについて厳密な担当者を決めずに動いていた時期もありました。具体的な担当者を決めずに運用することで状況に応じた柔軟な動きができる一方で、時間のあるときに表面的な改善を行う形にとどまってしまったり、腰を据えて取り組み必要がある改善が中々進まなかったり、といった課題がありました。そこで、現在ではそれぞれの分野に対して明確なアサインを決めるような形で運用しています。担当者はアサインされた分野に対して改めてリサーチを行い、具体的な改善計画の立案と推進を行います。これにより、明確な担当者を決めていなかった時期と比較するとより本質的な改善に時間を使える体制になりました。また、エンジニアとしてのキャリアアップしていくためには何らかの分野においてチームをリードするような経験が重要な要素になると考えており、マネージャーとしてそのような機会をチームに対して提供したいという思いもあり、現在のようにそれぞれの分野に対して明確なアサインを決めるような方式に落ち着いています。時間の確保先述のようにエウレカのエンジニアは業務時間のほとんどをプロジェクトでの仕事に使っているため、技術的な改善に使える時間はそこまで多くありません。そこで、Androidチームでは毎週火曜に技術的な改善にフォーカスする時間を確保し、原則その時間はプロジェクトの業務から離れ、Androidチームとしての改善タスクを進める時間としています。これによって忙しい通常業務の中でも一定の時間を改善タスクに使えるようになることに加え、物理的に同じ場所で作業していることで自然発生的に生まれる会話からより良いアイデアが生まれることもあり、単純に作業時間が確保されていること以上のリターンがあると感じています。まとめこの記事ではエウレカのAndroidチームが機能開発と技術改善をどのように両立させているかを紹介しました。マネージャーがボトルネックになることを避けるために、チーム全員で注力する分野を議論するような方式に移行Androidアプリ開発環境の変化に対応するために、定期的に優先順位の見直しを実施重要な分野に腰を据えて取り組むために、それぞれの分野に担当者をアサイン忙しい通常業務の中でも改善タスクに使える時間を確保するために、毎週改善タスクのためのスロットを確保うまくいっている部分もあれば、運用している中で以下のように課題が見えてきている部分もあり、また機会があれば続報を書きたいと思います。プロジェクトタスクと改善タスクをほぼ独立したものと扱っているが、両者をうまく連動させることができないか方針決定後の具体的なタスクでAIエージェントをもっと活用できないか機能開発と技術改善の両立 was originally published in Pairs Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.
この記事は「Pairs Engineering Advent Calendar 2025」の18日目の記事です。前回は Takeshi Watanabe さんによる「Developer Experienceの向上を目指して11万行のOpenAPI JSONをTypeSpecに移行した話」でした。こんにちは!Pairs の iOS Engineer の Takano です。近年は、弊社の iOS アプリでも本格的な SwiftUI への移行が日々進んでいます。その過程で、「宣言的 UI はなぜこのような姿になっているのか?」という疑問に何度も直面しました。使ってみると便利に感じる一方で、その設計の意図を言葉にしようとすると途端に曖昧になる … そんな経験をされる方も多いのではないでしょうか?今回の記事では、移行作業を通じて見えてきた 「設計の背景」や「理解のポイント」 を、試験問題風の問いを自分に投げかけながら整理してみます。単なるAPIの解説ではなく、「どうすればこの設計を咀嚼できるのか?」を一緒に考えていきましょう。宣言的 UI フレームワークである SwiftUI は、UIKit など従来の命令的な UI フレームワークと比べると取っ付きやすく、@State や View を書いて画面が更新される楽しさをすぐに味わえます。しかし、ある瞬間に手が止まることがあります。なぜこの形になっているのかを説明しようとすると、途端に言葉が足りなくなるのです。SwiftUI の思想は魅力的です。ただ「便利だ」「楽だ」と感じていても、なぜその設計になっているのかを言語化しようとすると途端に曖昧になります。今回のブログは、そうした設計に対して、試験問題的な問いを自分で作って解いてみるという試みです。ここでいう「試験問題的な問い」とは、他人を試すためではなく、自分自身の理解を確かめるために段階的な問いを設計する意味合いを持ちます。SwiftUI の機能を網羅的に解説するのではなく、「どうすればこうした設計を咀嚼できるか」を目指した方法論の実践として読んでいただければと思います。読者の想定このブログは以下のような読者を想定しています。SwiftUI を実務で使っており、@State や View は普段から書いている。しかし、なぜ View が値型なのか、なぜ状態が View の外にあるのかといった設計判断を、はっきりと言葉で説明しきれずにいる。「宣言的だから」「状態管理が楽だから」とは言えるものの、その必然性を自分の言葉で捉えきれていないと感じている。SwiftUI の API の使い方や基本的な解説を求めている方には、この文章は向いていません。対象読者はあくまで「使えているけれど説明しきれない部分が引っかかっている方」です。このような背景を踏まえ、次の章では SwiftUI の設計思想を説明しようとした際に感じた戸惑いを整理します。従来のフレームワークとの違いがどこにあるのか、それがどのように混乱を生むのかを見ていきます。設計思想を説明しようとしたときの戸惑い設計思想を言葉にしようとすると、従来の UI フレームワークとの発想の違いがいくつも浮かび上がり、戸惑いを感じました。UIKit のような命令的なフレームワークでは、画面の状態を直接操作しながら更新していきますが、SwiftUI では状態からビューを導出するという真逆の考え方に立っています。ビューは何度も再生成され、差分を計算して更新されるという動きも、初めて聞くと「そんなに頻繁に作り直して大丈夫なのか?」と驚くポイントです。さらに、状態管理のために @State や @ObservedObject などの property wrapper を使うスタイルは、宣言だけで裏側のライフサイクルが管理される点で、新鮮さと同時に不安も伴います。戸惑いの源泉を確かめるために、まずは従来のアプローチと SwiftUI の設計を対比し、その違いを整理します。従来の iOS の UIKit などのフレームワークでは、表示の更新は順序を持った命令列として記述されることが一般的でした。画面に変化を加えるたびに開発者が「どの操作を先に呼び出すべきか」「どの値を同期させるべきか」を意識する必要があり、更新漏れや順序の違いがバグの原因になりがちでした。しかし Apple が 2019 年に発表した SwiftUI は、UI の記述を現在の状態から導出する関数的な表現として位置付けています ¹。データが UI の中心となり、フレームワークが状態とビューの依存関係を追跡して、データの変更に応じた更新を自動で行います ²。宣言的なアプローチは、従来の命令型更新モデルに対する大きな転換です。命令型では「どのように」UI を更新するかを逐一記述しますが、宣言的 UI では「状態がこうであれば UI はこうなる」と結果を記述します ³。SwiftUI では状態が変わるたびに body プロパティが再評価され、フレームワークが前回の評価との違いを計算し、必要な部分だけを差分更新します ⁴。この diffing とデータ駆動の仕組みによって、状態と表示のズレを人間が手動で同期させる負担を減らせます。ただし、「表示は状態の関数」と言っても、実際に描画される要素にはライフサイクルがあり、SwiftUI の内部では各 View に紐づくオブジェクトツリーが維持されています。私たちが書く値型の View は宣言的な表現であり、フレームワークが差分を適用する際にはこの内部ツリーを用いて再利用可能な部分を保持しながら更新が行われます。値型の View が何度も再生成されても、状態を保持するオブジェクトや配置されているノードは適宜再利用されるため、宣言的に書きつつ効率的な更新が可能になっています。SwiftUI は @State や @ObservedObject などの property wrapper で状態を宣言します。@State はビュー内部で完結する小さな状態を保持し、その値が変わると body の再評価と差分更新を引き起こします。@ObservedObject や @StateObject は外部のデータモデルの変更を監視するために使われ、それらのオブジェクトが変化するとビューの更新をトリガーします ⁵。これらの仕組みのおかげで、開発者は更新の順序やタイミングを逐一記述する必要がなく、状態を宣言することに集中できます。ここまでで命令的モデルと宣言的モデルの違い、差分更新や property wrapper といった仕組みを概観しました。ただ、こうした技術的な違いを他人に伝えようとすると、どこまで理解できているかが曖昧で、説明に詰まる場面が出てきます。次は、設計思想を自分なりに噛み砕くためにどんな工夫が必要かを探っていきます。言葉に詰まる理由を探した結果、試験問題的な問いに行き着いた設計思想を言葉にしようとすると、感覚的には分かっていても説明が曖昧になる。そこで発想を反転させ、他人に説明する前に、自分がどこまで理解しているのかを測る側に回ってみることにしました。つまり、「どのレベルまで言えたら理解していると言えるのか」を問いの形で整理してみたのです。この試行錯誤の過程で、試験問題のように自分に問いを投げる形式が有効だと感じました。問いを作るには、自分の理解を複数の段階に分解する必要があります。実際にやってみると、次のようなレイヤーが浮かび上がりました:用語レベル ─ その設計で用いられる重要な用語を正確に選び取れるかどうか。概念レベル ─ 文脈に則して主要な設計概念を言い当てられるかどうか構造理解レベル ─ 設計の発想を実際に支える仕組みやデータの流れを理解しているかどうか。背景理解レベル ─ なぜその設計が必要になったのか、時代背景や技術的制約などを踏まえて理由を語れるかどうか。こうした階層を意識することで、自分がどこで理解が止まっているかが明確になりました。試験問題的な問いという形式は、単に暗記した知識を問うものではなく、理解の輪郭を炙り出す道具として機能します。試験問題的な問いの概要試験問題的な問いはここでは概要だけを紹介します。全文や解答例は末尾に掲載しています。この節で強調したいのは、問いを設計するためには理解を分解する必要がある点です。決して他者を試すためではなく、理解の足場を確認するためのツールとして使いました。用語判別 ─ UI を命令列として操作する体系と、状態から導出して記述する体系をそれぞれ何と呼ぶかを問います。語群穴埋め ─ 表示を実体ではなく関係として捉え、同一性を構造内の不変量に求める転換を読み取れるかを問います。短い論述 ─ 更新の正しさを保証する仕組みを、状態と表示の写像や差分更新といった構造として説明できるかどうか。背景理解を含めた論述 ─ なぜ人間の注意力に依存しない仕組みが必要だったのか、デバイスの多様化や非同期処理の一般化といった背景を踏まえて語れるかどうか。試験問題的な問いを通じて見えてきたこと実際に問題を作り、解答例まで用意してみると、以下のような発見がありました。表層的な説明と本質的な説明の違い ─ 用語の判別や表面的な特徴は分かっていても、その設計が解決したことや必然性を説明するには、時代背景や設計の目的を理解している必要があります。設計に纏わるパラダイムの必然性の理解 ─ 表示を実体ではなく状態との関係として捉え直す必要性を肌で理解できました。この発想の転換により、ビューを再生成しても状態が持続する構造が納得できるようになりました。また、非同期処理や複数デバイス対応といった現代の開発環境では、状態と表示のズレを人手で管理するのは限界があります。問いを通じて、データを第一級市民とし単一の真の所在 ⁶ にまとめる設計がなぜ必要なのか、背景まで含めて腹に落ちました。この理解は宣言的 UI というパラダイムが単なる流行ではなく、ある程度時代的な要請を汲んだ必然性を持つものであることを実感させます。解説ではなく問いが持つ機能 ─ 解説記事を書くと、どうしても答えを読み手に押し付けがちになりますが、問いを与えることで、読み手自身がどこで躓くかを自覚し、考えるきっかけを作ることができました。この効果は想像以上で、内部的な理解の進捗を測る物差しとして機能しました。なお、ここでいう「データを第一級市民とし単一の真の所在にまとめる」とは、データの重複を避けて一元的に管理し、そのデータを中心に UI を構築するという考え方を指しています。実際のプロダクトではキャッシュやネットワーク同期など複数のデータ源が存在することも多く、必ずしも「一箇所しか存在しない」というわけではありません。それでも状態を一元的に管理し、そこから UI を導出する仕組みを持つことで、表示と状態の不整合を減らせる、という意味合いです。SwiftUI から離れて広がる視点試験問題的な問いという形式は、SwiftUI に限らず、他のフレームワークや設計にも応用できます。例えば React や Jetpack Compose などの宣言的 UI、あるいは型システムや分散システムの設計でも、「理解しているつもりになっているけれど説明できない部分」を炙り出すのに役立つはずです。つまり、今回の試みは SwiftUI の思想を解説するためというより、「なぜこの形になっているのか」を説明しがたい設計に出会った時、どうやって理解の輪郭を掴むかを探る実践例、といった方が正確です。SwiftUI はたまたま題材として適していただけであり、目的は理解のプロセスそのものにあります。おわりに本記事は、SwiftUI の思想を解説することよりも、理解が難しい設計に対してどう向き合うか、どう言葉にするかを試行錯誤した記録です。試験問題的な問いを作るという回り道は、結果として理解の段階を自覚するための手段になりました。SwiftUI の思想に限らず、仕事や学びの中で「使えているのに説明できない」という場面は多いはずです。そんなときには、いきなり説明しようとするのではなく、段階的に問いを立てて理解を測ってみる方法が有効かもしれません。試験問題的な問いの全文と解答例以下は、本記事で説明した試験問題的な問いの全文と想定解答です。なお、ここに掲げるのは理解の段階を確認するための手法として用意したものです。決して他者の理解度を試したり順位づけしたりするためではなく、自分自身がどこまで理解できているかの足場を確かめるためのツールとして使っています。用語の理解、概念の捉え方、構造の把握、背景の理解を段階的に測り、問いに答えるプロセスを通じて自分の理解の進捗を知るためのものです。また、ここで提示する内容はあくまで思考のきっかけを提供するためのものであり、正確性や完全性を保証するものではありません。多少の省略や抽象化についてはご容赦いただき、単に正解を当てるクイズではなく、理解を深めるための取っ掛かりとしてご活用ください。資料文20世紀後半、計算機は専門家の道具から一般市民の生活基盤へと変貌した。この変化に伴い、人間と計算機の接点である「UI」は単なる出力装置ではなく、アプリケーションの状態を反映する仕組みとして捉え直す必要が生じた。従来の UI 技術では、画面に表示される要素は長時間存在し、開発者が命令列を積み上げて状態と表示を同期させることが一般的だった。しかし、デバイスの多様化や非同期処理の一般化が進むと、こうした手法では更新漏れや責務の分散、保守性の低下といった問題が顕在化した。こうした背景のもと、21世紀初頭には「UI は状態から導出される」という設計思想が提唱された。この考え方では、UI を独立した実体ではなく状態との関係として捉え、値型のビューディスクリプションを何度でも再評価できるものとし、永続すべきものはアプリケーションデータそのものとする。この枠組みでは、UI の同一性は固定されたオブジェクトに由来するのではなく、内部で維持される構造に現れる不変量に基づいて評価される。その結果、設計者は次のような課題に直面することとなった。ビューが再生成されてもデータが保たれる仕組みが必要であるデータの変化を UI に正確かつ自動的に反映する仕組みが必要であるデータの責任範囲を明示し、混乱を避ける必要がある非同期処理や時間経過を踏まえて一貫性を保つ構造が求められるこれらの課題に応えるため、設計者はビューとデータのライフサイクルを分離し、UI を状態の関数として記述するコード構造を採用した。問題単語解答:資料文に示された「表示を命令の逐次実行によって更新する体系」と「表示を状態の関数として定義する体系」は一般的にそれぞれどのような記述様式と呼ばれるか。語群穴埋め:資料文中の設計思想では、表示は独立した(ア)ではなく状態との(イ)として捉えられる。その結果、同一性は固定的な存在ではなく、観測される(ウ)のなかに現れる(エ)に基づいて定義される。語群:構造 / 関係 / 不変量 / 実体短い論述(80字以内):「表示を状態の関数として定義する」という設計は、更新の正しさをどのように保つか。従来の更新モデルとの差異に着目し、80字以内で述べよ。背景を含めた論述(140字以内):資料文の設計思想は、なぜ人間の注意や手作業に依存しない正しさを志向したのか。装置の多様化や時間軸上の変化を前提として、設計思想としての必然性を140字以内で論じなさい。想定解答単語解答 / 解答例:命令的・宣言的語群穴埋め / 解答例:(ア)実体、(イ)関係、(ウ)構造、(エ)不変量短い論述(80字以内)/ 解答例:従来の命令的な体系では操作順序に依存したが、状態から導出する宣言的な設計では同じ状態は常に同じ表示に写像され、差分計算で更新の正しさが保証される。背景を含めた論述(140字以内)/ 解答例:デバイスや入力方法が多様化し、非同期処理が一般化したことで、UI 更新の複雑さが増し、命令的な方法では更新順序やタイミングを人が管理しきれず不整合が起きやすくなった。そのため、状態を中心に UI を導出するなど、正しさを構造に委ねる設計が必要になったから。解き方と読み筋最後に、今回用意した試験問題的な問いをどう読み解き、どう答えたらよいかの目安をまとめます。この試験問題の目的は、具体的な API 名ではなく構造と思考の流れに焦点を当てることにあります。前半で紹介した四つのレイヤー(用語・概念・構造理解・背景理解)に沿って、自分の理解を分解します。単語解答では、UIKit のように命令列でビューを操作するアプローチを「命令的」、SwiftUI のように状態を宣言して描画するアプローチを「宣言的」と呼び分けます。宣言的モデルでは、開発者は「どう描くか」ではなく「何を描きたいか」を記述します。語群穴埋めでは、表示を固定的な「実体」と考える従来の捉え方から、状態との「関係」として捉え直す転換を問うものです。同一性は構造の中の不変量として定義されると考えます。これにより、「ビューが再生成されても同じ状態なら同じものとみなせる」という考え方が導かれます。80 字論述では、従来モデルと比較しつつ、状態と表示の写像や差分計算が更新の正しさをどのように担保するか、構造の仕組みに終始して簡潔にまとめます。140 字論述では、対照的に、非同期処理の一般化やデバイスの多様化といった環境的な要請を踏まえて、なぜ命令的な方法では限界があるのかという背景に終始して論じます。ここで両者の役割をはっきり分けることが、読みやすさと理解の助けになります。想定解答は一例に過ぎません。特に論述問題では、構造や背景を自分なりの言葉で噛み砕くこと、正確さにこだわるよりも、なぜそのような設計が必要になり、どう機能しているのかを自分の中で腑に落とすことを重視してください。参考リンク¹ ² ⁶ Data Flow Through SwiftUI — WWDC19 — Videos — Apple Developer https://developer.apple.com/videos/play/wwdc2019/226/³ ⁴ The Shift From Imperative to Declarative UI — Increment https://increment.com/mobile/the-shift-to-declarative-ui/⁵ Exploring Key Property Wrappers in SwiftUI — @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, and @Environment https://fatbobman.com/en/posts/exploring-key-property-wrappers-in-swiftui/<img src="h
この記事は Eureka Advent Calendar 2025 の 17日目の記事です。こんにちは。Pairs の Back-end Engineerのbuzzです。2025年はPairsがきっかけでお付き合いを始めた彼女と結婚し、ヨーロッパに新婚旅行に行ったり、家を購入したりとプライベートがとても充実した1年でした。プライベートだけでなく仕事も充実していたので頑張った取り組みを振り返りつつ書いていきます。はじめにPairsでは長年OpenAPIをJSON形式で運用してきました。しかし、合計11万行を超えるまでJSONが肥大化し、編集の困難さやLLMツールでの読み込みエラーなど、様々な問題が顕在化していました。本記事では、この課題を解決するために選んだ「TypeSpec」への移行についてご紹介します。2023年の記事の続編です。Pairs のAPI 開発における OpenAPI 運用の改善と振り返り移行の背景PairsではOpenAPIを以下の用途で活用しています。APIドキュメント:Redoclyで生成したHTMLドキュメントをチーム間で共有コード生成:スキーマからGoのバリデーションコードを自動生成仕様の信頼できる情報源(Single Source of Truth):Backend・iOS・Android・Web Frontend チーム間の認識合わせエンドポイントが増えるたびにJSONファイルは肥大化し続け、メンテナンスの限界を迎えていました。課題点は大きく以下の3つです。課題点①:LLMツールで扱えないPairsではClaude CodeなどのLLMツールを開発に活用するチームが増えています。しかし、数万行のJSONファイルをLLMに読み込ませると、コンテキストの上限を超えてエラーになってしまいます。これは現代の開発フローにおいて致命的でした。課題点②:開発体験の悪化LLMが使用できない我々に残された選択肢はテキストエディタでJSONを直接編集することでした。しかし巨大なJSONファイルには以下のような問題点があります差分が見づらい:PRレビュー時、変更箇所の把握が困難構文エラーのリスク:カンマ一つ忘れただけで全体が壊れるIDEサポートが弱い:補完やバリデーションが効きにくいちょっとした修正でも気が重くなり、開発体験は低下している状態でした。課題③:運用が一部の有識者に偏る上記の開発体験の悪化から、OpenAPIの編集はナレッジのある一部の有識者に集中するようになりました。巨大JSONへの心理的ハードルが高さから、本来であれば全エンジニアも仕様変更に関われるはずが、事実上有識者達の専任作業になり、組織全体のボトルネックになりつつありました。これらの課題が同時に押し寄せたことで、本格的な対策を検討し始めました。解決策のTypeSpecテックリードやマネージャーと議論した結果、「OpenAPIは共通言語として残しつつ、開発体験を改善したい」という方針に決まりました。複数の選択肢を比較検討し、TypeSpecへの移行を決断しました。TypeSpecは、Microsoftが開発したAPI仕様記述言語です。TypeScriptに似た構文でAPIを定義し、OpenAPI・JSON Schema・Protobufなどの形式にコンパイルできます。主に以下のメリットがあり、 TypeSpecは私たちの要件に合致していました。型安全・IDE補完コンパイル時に型チェックが行われ、VS Code拡張による補完も強力。開発体験の大幅な向上が見込めました。Microsoftがメンテナンス 長期的なサポートが期待でき、突然の終了リスクが低いです。LLMとの相性がいい文法が少なく、パターンも少なく、記述量が少ないためLLMとの相性が良いです。記述量の大幅削減記述量が約1/5ぐらいになりそうでした。OpenAPIへのコンパイルが可能既存資産を活かしながら、移行ができます。具体的には以下の違いがあります。記述量が明らかに少なくなることがわかるかと思います。GET /2.0/users/{user_id} by TypeSpec@tag("User")@route("/2.0/users/{user_id}")@get@summary("Get user by ID")op getUserById(@path user_id: integer): UserResponse;GET /2.0/users/{user_id} by JSON{ "paths": { "/2.0/users/{user_id}": { "get": { "summary": "Get user by ID", "parameters": [ { "name": "user_id", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserResponse" } } } } } } } }}移行手順ここでは、実際の移行の流れを紹介します。1️⃣の部分をJSON→TypeSpecに移行し、後続の生成の流れを変えないことがゴールです。OpenAPIの生成の流れStep 1:環境構築TypeSpecの実行にはNode.js(18.x以上)が必要です。また、VS CodeまたはCursorにtypespec-vscodeをインストールすることで、シンタックスハイライト・オートコンプリート・エラー検出が利用可能になります。npm install @typespec/compilernpm install @typespec/openapi3Step 2:ディレクトリ構成の設計以下のような構成を採用しました。typespec/ ├── main/ # Pairs Main API の TypeSpec 定義 │ ├── main.tsp # エントリーポイント(名前空間、設定) │ ├── models.tsp # データモデル定義 │ └── operations.tsp # エンドポイント定義...Step 3:既存yamlからのTypeSpecを生成あとはこのコマンドで一気に移行するだけだ。と思ったのですが、コマンドを打つ前からyamlにエラーが出ている状態だったのもあり、大量にエラーが出てうまくいきませんでした。 tsp-openapi3 ./openapi.yaml --output-dir ./main.tsp移行時に遭遇したエラー1: PrettierParserError: Unterminated string literal.主に以下の3つのものでした。“ ’ ` で 開いたのに閉じる “ ‘` がない。文字列が長すぎてPrettierが長い行を改行しようとして、文字列リテラルの途中で切ってしまう。文字列に改行コード(\n)が含まれておりエラーになる。2:一部の型がunknownに変換される。openapi.yaml で type:object の宣言が抜けていると TypeSpec への変換時に unknownになってしまいました。3:namespaceのバッティング以下のようなDeviceに対する定義が複数あるとnamespaceのバッティングが起きます。そのため、どちらかをリネームするか片方に統一する必要があります。model Device { type: string;}enum Device { ios, android, pc, sp}その他いろいろなエラーが100件ほど出ましたが、気合いで直し無事compile commandを通すことができました。これにてJSON→YAMLと生成していたものがTypeSpec→YAMLに置き換え可能になりました。tsp compile typespec/main --config tspconfig.yaml --output-dir gen/main/doc --option \"@typespec/openapi3.output-file={output-dir}/pairs-api-openapi.yaml\"Step 4:CI/CDへの組み込み最後に既存のCI/CDに組み込みます。コミット時に自動でコンパイル・フォーマットが走るようにpre-commitフックを設定しました。pre-commitでは、TypeSpecコンパイル + フォーマット + Goコード生成 + OpenAPI YAML生成をするようにしています。フォーマットは以下のコマンドで実現しています。tsp format "**/*.tsp"これにより開発者はTypeSpecを書くことだけに専念すれば良いことになります。そしてTypeSpecを書いて、PR上で最終成果物のHTMLをGitHub Pagesにホスティングし、全プラットフォームのエンジニアで認識を合わせることができます。PRで確認できる最終成果物のHTML導入効果TypeSpec移行によって課題だった点が以下のように解決しました。課題点①:LLMツールで扱えないファイルサイズが小さくなったことで、Claude Codeでも問題なく扱えるようになりました。さらに、Claude Codeコマンドを用意し、対話形式でエンドポイント定義を自動生成できる仕組みも構築し、API仕様の追加がより手軽になりました。課題点②:開発体験の悪化 TypeSpecの読みやすい構文と、11万行を超える巨大なJSONファイルが1file最大1万行程度の扱いやすいtspファイル変わり、作業者、レビューワー双方の負担を軽減することができました。課題点③:運用が一部の有識者に偏る有識者だけでなく全プラットフォームのエンジニアがOpenAPIが書きやすい環境を作ることができ、運用の属人化が解消されつつあります。久しぶりにOpenAPIを書いたエンジニアにも前よりずっと書きやすいと言ってもらえました。まとめ巨大に膨れ上がったOpenAPI JSONのLLMツールの非対応、開発体験の悪化、属人化、それら複数の課題を解決するために、Open API をJSONからTypeSpecに移行しました。記述量は約1/4に削減され、IDE補完やLLMがサクサク動く編集環境を手に入れました。そして何より、全エンジニアがAPI仕様に関われる体制が整いました。今後の課題としてまだまだ、OpenAPIに情報量が不足しており、SSOTのドキュメントとしては心許ないところがあります。またドキュメントとしてのクオリティが上がれば、Goを始めとしたコードの自動生成化をより加速できるので、今後も開発組織全体でOpenAPIを育てていければと思っています。Developer Experienceの向上を目指して11万行のOpenAPI JSONをTypeSpecに移行した話 was originally published in Pairs Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.
I have a little confession to make: I am a bit envious of my product analytics colleagues. They get to work with A/B tests, the gold standard of causal inference with clean counterfactuals, huge sample sizes, and parameters that are well behaved. Meanwhile, in the marketing science pit, we are knee-deep in observational data, trying to extract causality from a chaotic mixture of covarying spend patterns, promotional events, seasonality, competitor actions, macro factors and noise. You know, the usual Tuesday where seasonal campaign spend, Valentine’s Day promotions, and a competitor’s price cut all happened in the same week.But we keep trying anyway, because marketers need to know whether their campaign worked, and if so, how much. With multi-touch attribution gone, Media Mix Modeling (MMM) has come roaring back. There are now many open-source libraries out there, like Facebook’s Robyn, Google’s Meridian, and PyMC-Marketing by PyMC Labs. But the resurgence hides an uncomfortable truth:Modern MMM often has more parameters than information to estimate them.Let’s talk about why.Do you want to build a sandwich?On paper, the core idea of MMM is fairly straightforward:You have a KPI time series. It goes up and down. Cute.Take your media spend time-series, transform them by applying adstock (the lagged effects of an ad) and saturation functions of your choiceStack the transformed signals and regress them against the KPI.It’s basically a line fitting optimization problem. How hard can it be?Very, as it turns out.These transformations are nonlinear, correlated, and lagged, which results in a problem called non-identifiability. This means that you can have a set of solutions that give you really good, equally optimal fit but no way to tell which one is truer based on the data.To illustrate this, imagine you’re an alien who has never been to Earth. Your friend, who just finished a vacation there (“it’s beautiful this time of the year!”), told you about this wonderful human delicacy called the BLT: the glorious bacon-lettuce-tomato sandwich. You are intrigued, so of course you go to your alien artificer wonder machine to try to replicate it. You have learned from your friend that this BLT sandwich hasTotal weight of 200gMade of toast, bacon, lettuce, tomato, and mayonnaise… but that’s all you know. Your friend didn’t tell you anything else. Now, with this information, can you make a sandwich?Well, unfortunately no. With these constraints you can end up with sandwiches that are 140g toast, 10g rest of the ingredients and still satisfies the conditions.To get anywhere close to a sane sandwich you will need more information, for example an approximate ratio of lettuce, tomatoes, bacons, etc. This is MMM’s problem in a nutshell. The KPI is the sandwich weight, and the distribution of media effects is potentially unknowable. Just as there are infinite ways to distribute 200g across sandwich ingredients, there are infinite ways to distribute KPI lift across correlated media channels that all ramped up together during the holiday season.Multiple parameterizations can fit the same KPI equally well. Despite identical goodness-of-fit metrics (RMSE), each model attributes the KPI lift very differently across channels. This is the core non-identifiability problem in Media Mix Modeling.You might think, “okay, so the model can’t perfectly separate effects. But if the total is right, isn’t that good enough?” Not quite. The whole point of MMM is to inform budget allocation decisions: which channels deserve more spend, which should be cut? If the model says “Facebook drove 40% of lift, and Instagram drove 20%” but it could just as easily be “Instagram drove 40%, Facebook drove 20%” with the same fit quality. You’re making million-dollar decisions on a coin flip.This is where modern MMM frameworks step in with different philosophies on how to break the tie.Robyn and Meridian — Different Approaches to the Same ProblemWhat is needed to break out of the non-identifiability zone and into something resembling a robust model is additional information: what you already kind of know, something that can help you restrict the plausible range of the parameter values.Looking at it this way, we can see that encoding additional information into the model fitting process is practically required when dealing with large number of parameters. Robyn and Meridian are two different MMM implementations that try to solve the same problem with its own way of encoding prior information.Robyn: Multi-objective survivalRobyn tackles identifiability through multiple objective functions: fit, realistic spend/effect distribution (DECOMP.RSSD), and calibration alignment. The model that gets selected need to be Pareto optimal, i.e. good model by all criteria. It’s essentially semi-Bayesian, with priors expressed indirectly through calibration constraints and regularization penalties.Meridian: Full Bayesian UncertaintyMeridian takes on the uncertainty more directly with a Bayesian approach. You encode your pre-modeling information in the form of priors, and the MCMC sampling algorithm explores the parameter space to produce posterior distributions based on your priors and the data. The resulting posterior distributions show you the range of plausible parameter values. Wide posteriors reflect true ambiguity in the data, while narrow posteriors indicate the data was informative. Where data is weak, priors exert stronger pull on the final estimates.Some of you might be wondering, “well jeez, I’m doing MMM because I don’t know about the media contribution of the channel I want to know about,” which is true and fair. To be frank, these prior information come from a messy intersection of platform characteristics, historical learnings, geo-lift experiments, domain knowledge, cross-functional context, and modeling judgment. And this is exactly the point: building effective MMM is less of a precise science and more of an art of systematically integrating what you do know.MMM as Part of an Triangulation SystemHere is the most important shift in mindset most marketers need to have about MMM: it’s not a ground truth generator, where you input your historical data and out comes a clean estimation of how much the marketing channels contribute to your KPI. MMM acts best when it’s used as an integrator of information that systematically incorporates what you know, and how confidently, about your marketing campaigns.This is why our analytics team here at Pairs, and Match Group in general, are moving towards a triangulation approach where we use multiple sources of information to help pin-point the estimate:1. Fast-moving OrbitML based Bayesian Structural Time SeriesWe use BSTS models to quickly capture channel efficiency signals on a rolling basis. The “fast-moving” part is key. These models update weekly and can detect shifts in channel performance much faster than traditional MMM refresh cycles. Bayesian structural time series naturally decompose the KPI into trend, seasonality, and channel effects using state-space models, giving us a baseline understanding of what’s driving changes week-to-week.2. Selective geo-holdout testsFor high-stake, high-ambiguity channels, for example a new upper-funnel TV campaign where we genuinely don’t know if it works, we run geo-holdout experiments. We hold back spend in randomly selected markets and measure the difference. This gives us quasi-experimental, causally credible estimates that become strong priors in the MMM. For example, if a geo-test shows YouTube brand campaigns deliver a 1.5x-2.0x ROAS, we can encode this as a prior distribution in the MMM model.3. Last-touch attribution and platform metricsWhile last-touch attribution has well-known biases (it over-credits lower-funnel channels), it still provides a useful supplemental signal. We don’t, and shouldn’t, treat platform-reported conversions as ground truth, but we do use them as loose upper or lower bounds. If Google Ads reports 1000 conversions and MMM allocates 50, something is probably wrong.4. MMM with integrated priorsFinally, we run the full MMM model with learnings from steps 1–3 encoded as priors. The OrbitML estimates inform our expectations for baseline channel efficiency. The geo-test results become tight priors on specific channels. The attribution metrics serve as soft calibration targets. The MMM then produces a holistic view that respects all these partial truths while maintaining internal consistency about how channels interact (saturation, adstock, synergies).This way, MMM isn’t treated as oracle (which it cannot be), but a model that weaves together all partial truths.Final ThoughtsMarketing effectiveness will never be perfectly knowable. User journeys and how our marketing touch points interact with them are highly complex and non-linear. It is no wonder that even with modeling technique as sophisticated as MMM, historical data alone will never give you the guaranteed truth. But utilized wisely, MMM can be a powerful platform to integrate all the loose infor