TypeORMの保存メソッドをSQLとパフォーマンスで比較してみた

はじめに

こんにちは、EventHubの成瀬です。

TypeORMでデータを保存するとき、 save() / insert() / update() / upsert() の使い分けに迷ったことはないでしょうか?あるいはINSERTもUPDATEもやってくれる save() をとりあえず使っていませんか?

挙動の違いはなんとなく理解しつつも、「このケースでどれを使うべきか」を根拠とともにはっきり説明しきれないもやっとした感覚がありました。開発中に選択を迷う場面が何度かあったこともあり、一度整理して、発行されるSQLやパフォーマンスの差も把握しておこうと思ったのがこの記事のきっかけです。

この記事では実際に発行されるSQLとベンチマーク結果をもとに、各メソッドの特性と使い分けを整理します。 なお、TypeORMはActive RecordとData Mapperの両パターンをサポートしていますが、本記事ではRepository APIを使うData Mapperパターンを前提としています。

各メソッドの概要

まず各メソッドを簡単に整理します。

メソッド 概要
save() オブジェクトにPKが存在すればUPDATE、なければINSERT
insert() INSERTのみ
update() UPDATEのみ
upsert() INSERT ... ON DUPLICATE KEY UPDATE(新規・更新を1クエリで処理)

実際に発行されるSQL

TypeORMの logging: ["query"] を有効にして確認しました。

なお、本記事の確認環境はMySQL 8です。他のデータベース(PostgreSQLなど)では発行されるSQLが異なります。

検証に使用したEntityとテーブル

@Entity("samples")
class Sample {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  name!: string;

  @Column()
  value!: number;

  @CreateDateColumn()
  createdAt!: Date;

  @UpdateDateColumn()
  updatedAt!: Date;
}

生成されるDDL

CREATE TABLE `samples` (
  `id`        int          NOT NULL AUTO_INCREMENT,
  `name`      varchar(255) NOT NULL,
  `value`     int          NOT NULL,
  `createdAt` datetime(6)  NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  `updatedAt` datetime(6)  NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

save()(新規登録)

-- save() : 1件
query: START TRANSACTION
query: INSERT INTO `samples`(`id`, `name`, `value`, `createdAt`, `updatedAt`) VALUES (DEFAULT, ?, ?, DEFAULT, DEFAULT) -- PARAMETERS: ["item-0",0]
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`createdAt` AS `Sample_createdAt`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` = ? -- PARAMETERS: [1]
query: COMMIT

-- save([]) : 配列
query: START TRANSACTION
query: INSERT INTO `samples`(`id`, `name`, `value`, `createdAt`, `updatedAt`) VALUES (DEFAULT, ?, ?, DEFAULT, DEFAULT) -- PARAMETERS: ["item-0",0]
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`createdAt` AS `Sample_createdAt`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` = ? -- PARAMETERS: [1]
query: INSERT INTO `samples`(`id`, `name`, `value`, `createdAt`, `updatedAt`) VALUES (DEFAULT, ?, ?, DEFAULT, DEFAULT) -- PARAMETERS: ["item-1",1]
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`createdAt` AS `Sample_createdAt`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` = ? -- PARAMETERS: [2]
query: COMMIT
  • トランザクション内でINSERTとSELECTが追加され、1回で4クエリです。
  • 配列渡しでは1トランザクション内でINSERTとSELECTが1件ずつN回繰り返され、N件で2N+2クエリになります。

save()(既存更新)

-- save() : 1件(差分あり)
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`name` AS `Sample_name`, `Sample`.`value` AS `Sample_value`, `Sample`.`createdAt` AS `Sample_createdAt`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` IN (?) -- PARAMETERS: [1]
query: START TRANSACTION
query: UPDATE `samples` SET `value` = ?, `updatedAt` = CURRENT_TIMESTAMP WHERE `id` IN (?) -- PARAMETERS: [100,1]
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` = ? -- PARAMETERS: [1]
query: COMMIT

-- save() : 1件(差分なし)
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`name` AS `Sample_name`, `Sample`.`value` AS `Sample_value`, `Sample`.`createdAt` AS `Sample_createdAt`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` IN (?) -- PARAMETERS: [1]

-- save([]) : 配列(差分あり)
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`name` AS `Sample_name`, `Sample`.`value` AS `Sample_value`, `Sample`.`createdAt` AS `Sample_createdAt`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` IN (?, ?) -- PARAMETERS: [1,2]
query: START TRANSACTION
query: UPDATE `samples` SET `value` = ?, `updatedAt` = CURRENT_TIMESTAMP WHERE `id` IN (?) -- PARAMETERS: [200,1]
query: UPDATE `samples` SET `value` = ?, `updatedAt` = CURRENT_TIMESTAMP WHERE `id` IN (?) -- PARAMETERS: [201,2]
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` = ? -- PARAMETERS: [1]
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` = ? -- PARAMETERS: [2]
query: COMMIT
  • 更新時は存在チェックのSELECTが先頭に加わり、1回で5クエリです。
  • 既存エンティティを渡した場合、DB値とオブジェクトで全フィールドの値が同じであればUPDATEを発行せず(ダーティチェック)、トランザクションも開始されません。( updatedAt も変わりません)
  • 配列渡しでは存在チェックが1クエリにまとまり、1トランザクション内でUPDATEとSELECTそれぞれN回ずつで、N件で2N+3クエリになります。

insert()

-- insert() : 1件
query: INSERT INTO `samples`(`id`, `name`, `value`, `createdAt`, `updatedAt`) VALUES (DEFAULT, ?, ?, DEFAULT, DEFAULT) -- PARAMETERS: ["item-0",0]
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`createdAt` AS `Sample_createdAt`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` = ? -- PARAMETERS: [1]

-- insert([]) : 配列
query: INSERT INTO `samples`(`id`, `name`, `value`, `createdAt`, `updatedAt`) VALUES (DEFAULT, ?, ?, DEFAULT, DEFAULT), (DEFAULT, ?, ?, DEFAULT, DEFAULT) -- PARAMETERS: ["item-0",0,"item-1",1]
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`createdAt` AS `Sample_createdAt`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE (`Sample`.`id` = ? OR `Sample`.`id` = ?) -- PARAMETERS: [1,2]
  • 件数に関わらず1回あたりINSERTとSELECTの2クエリです。

update()

query: UPDATE `samples` SET `value` = ?, `updatedAt` = CURRENT_TIMESTAMP WHERE `id` IN (?) -- PARAMETERS: [100,1]
  • 1回あたりUPDATEの1クエリで、存在チェックはありません。
  • @UpdateDateColumn がある場合、TypeORMが自動的に updatedAt = CURRENT_TIMESTAMP をSET句に含めます。

upsert()

-- upsert() : 1件(新規)
query: INSERT INTO `samples`(`id`, `name`, `value`, `createdAt`, `updatedAt`) VALUES (DEFAULT, ?, ?, DEFAULT, DEFAULT) ON DUPLICATE KEY UPDATE `id` = VALUES(`id`), `name` = VALUES(`name`), `value` = VALUES(`value`) -- PARAMETERS: ["item-0",0]
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`createdAt` AS `Sample_createdAt`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` = ? -- PARAMETERS: [1]

-- upsert() : 1件(更新)
query: INSERT INTO `samples`(`id`, `name`, `value`, `createdAt`, `updatedAt`) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE `id` = VALUES(`id`), `name` = VALUES(`name`), `value` = VALUES(`value`), `createdAt` = VALUES(`createdAt`), `updatedAt` = VALUES(`updatedAt`) -- PARAMETERS: [1,"item-0",100,"2026-04-24T03:49:42.387Z","2026-04-24T03:49:42.387Z"]
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`createdAt` AS `Sample_createdAt`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE `Sample`.`id` = ? -- PARAMETERS: [1]

-- upsert([]):配列(新規)
query: INSERT INTO `samples`(`id`, `name`, `value`, `createdAt`, `updatedAt`) VALUES (DEFAULT, ?, ?, DEFAULT, DEFAULT), (DEFAULT, ?, ?, DEFAULT, DEFAULT) ON DUPLICATE KEY UPDATE `id` = VALUES(`id`), `name` = VALUES(`name`), `value` = VALUES(`value`) -- PARAMETERS: ["item-0",0,"item-1",1]
query: SELECT `Sample`.`id` AS `Sample_id`, `Sample`.`createdAt` AS `Sample_createdAt`, `Sample`.`updatedAt` AS `Sample_updatedAt` FROM `samples` `Sample` WHERE (`Sample`.`id` = ? OR `Sample`.`id` = ?) -- PARAMETERS: [1,2]
  • 件数や新規・更新に関わらず1回あたりINSERTとSELECTの2クエリです。
  • MySQLでは INSERT ... ON DUPLICATE KEY UPDATE に変換されます。
  • 補足として、既存レコードの更新時に save()update()updatedAt = CURRENT_TIMESTAMP を自動でセットするのに対し、 upsert() はエンティティが持つ updatedAt の値をそのままVALUESに渡すため、 updatedAt は自動更新されません。

パフォーマンス計測

実行環境

  • CPU: Apple M2 Pro
  • Node.js: v22.12.0
  • TypeORM: 0.3.14
  • DB: MySQL 8(ローカル)

ウォームアップ1回のあと同一操作を5回実行し、中央値を計測結果としています。また各ケースの計測前にテーブルをクリアして初期状態に戻しています。

今回はローカルDB接続での参考値のため、本番環境(リモートDB)ではネットワーク遅延がクエリごとに加算され、特にループ系の差はさらに大きくなることが予想されます。

INSERT系

操作 100件 1000件
save() ループ(新規登録) 169ms 1321ms
insert() ループ 118ms 1019ms
save([]) 配列(新規登録) 38ms 299ms
insert([]) 配列 3.0ms 24ms
  • 最速は insert([]) 、最遅は save() ループで、1000件の差は約55倍になりました。
  • ループ系同士では save() より insert() が速く、差は save() が1件ごとにSTART TRANSACTION/COMMITを発行することによる余分な2往復のラウンドトリップによるものと考えられます。
  • insert() ループより save([]) が速いのは、クエリ数はほぼ同じ(2N vs 2N+2)ながら1トランザクションにまとめることで redoログフラッシュinnodb_flush_log_at_trx_commit=1 のデフォルト動作)がN回→1回に減ることが主な原因として考えられます。autocommitではN回COMMITが走り、そのたびにディスクへの物理書き込みが発生します。
  • save([]) より insert([]) が大幅に速いのは、全件を1クエリにまとめて戻り値取得のSELECTも1回で完結するためです。

UPDATE系

操作 100件 1000件
save() ループ(既存更新) 174ms 1490ms
update() ループ 83ms 774ms
save([]) 配列(既存更新) 38ms 752ms
upsert([]) 配列 5.4ms 20ms
  • 傾向としてはINSERT系と同様になりました。
  • 最速は upsert([]) 、最遅は save() ループで、1000件の差は約75倍になりました。
  • save([]) のINSERT/UPDATEでそれぞれクエリ数はほぼ同じ(2N+2 vs 2N+3)ですが1000件でUPDATE系が遅くなっている理由としては、そもそも単体での処理比較としてINSERTよりUPDATEが遅い(undoログへの旧値書き込みなど)のが積み重なった結果と考えられます。

補足:戻り値取得のSELECTをスキップする

save()insert() で戻り値が不要な場合はオプションでSELECTをスキップできます。

await repo.save(entity, { reload: false });
// INSERT後のSELECTが消え、1件あたり3クエリになる
// START TRANSACTION → INSERT → COMMIT

insert() には直接オプションがないため、クエリビルダ経由で指定します。

await dataSource
  .createQueryBuilder()
  .insert()
  .into(Sample)
  .values(items)
  .updateEntity(false)
  .execute();
// バルクINSERT後のSELECTが消え、常に1クエリになる

INSERT系での計測結果(1000件)

操作 デフォルト reload:false / updateEntity:false
save() ループ(新規登録) 1321ms 1088ms (−17.6%)
save([]) 配列(新規登録) 299ms 151ms (−49.5%)
insert([]) 配列 24ms 9.1ms (−62.1%)

SELECTが消えたことでいずれも処理時間が短くなっています。

特に insert([]) はSELECTの対象が WHERE id=1 OR id=2 OR ... OR id=N と件数分展開されるため、大量件数では影響が大きくなります。バッチ処理など後続で生成値を使わないケースでは updateEntity(false) を検討する価値がありそうです。

使い分けガイド

用途 単体 一括
新規のみ insert() insert([])
更新のみ update() upsert([])
新規・更新混在 upsert([])
cascade / 更新差分チェック save() save([])
  • ループ内で1件ずつ処理するのは最もコストが高いパターンです。複数件の一括処理が絡む場面ではまず insert([]) / upsert([]) を検討するのが良さそうです。
  • save()を使いたい場面としては、cascade設定のあるリレーションを一緒に保存したい場合や、更新時の差分チェックをTypeORMに任せたい場合などがありそうです。それ以外の場面では、より明示的かつクエリ数も少なく済む insert() / update() が選択肢になります。
  • upsert()@UpdateDateColumn の自動更新なし。呼び出し前に updatedAt を明示的にセットする必要があります。

おわりに

各メソッドの発行SQL・挙動の違い・パフォーマンスを一通り整理してみました。

ORMはSQLを抽象化してくれて便利ですが、実際にどんなSQLが走っているかを把握しておくことは、パフォーマンス問題のデバッグや意図しない挙動の回避において依然として重要です。

計測結果からも、特に一括処理では適切なメソッドを選ぶことでパフォーマンスが大きく改善できることが確認できました。

メソッドを使い分けることで、コードの意図も伝わりやすくなるという副次的なメリットもあります。すべて save() ではなく適切な状況で insert()update() を使うことで「新規専用」「更新専用」という意図がコードから一目でわかるようになります。

今回確認した一部の細かい挙動は公式ドキュメントに明記されていないものもありました。実際の動作を正確に把握したい場合はTypeORMのソースコードを直接読むのが確実です。特に以下のファイルが参考になります。

現在、EventHubではエンジニアを募集しています。本記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談からご連絡ください!

jobs.eventhub.co.jp

note.com

参考リンク

Dockerイメージのサイズ削減と開発改善の話

はじめに

こんにちは。EventHubの西内です。

EventHubというサービスは2019年7月にリリースされ、まだまだ開発したい機能はあるものの、ある程度の顧客ニーズをカバーできるぐらいには機能が出揃って来たという状況です。

一方で、サービスが成長・拡大していく中で、最新技術への継続的なアップデートや、さらに高いパフォーマンスを出すための最適化など、中長期的な視点で取り組むべき課題も生まれてきました。

そういった背景や状況を踏まえ、顧客へ直接的な価値を提供する機能開発をやりつつも、「開発改善」を行う時間の割合を増やすことにしました。

そのうちの施策の1つとして、今回はDockerイメージのサイズ削減に取り組んだ話について少し触れていこうと思います。

Dockerイメージのサイズ削減

前提として、EventHubではフロントエンドもバックエンドもTypeScriptを採用しており、さらにyarn workspaceを利用したmonorepoでの管理を行っています。複数のワークスペースのうちの1つがAPIサーバー(とBatch処理)を担っています。 今回はそのAPIサーバーが利用するimageのサイズを小さくしようという話になります。

方針として「比較的安全にDockerイメージのサイズを削減する」ことにしました。

Dockerイメージに関わる課題感はいくつか思い浮かぶものがあるとは思いますが、サイズ削減だけにフォーカスしたのは改善前の時点でイメージのサイズが約5GBにもなっていたためです。

また、比較的安全としているのは、サイズ削減を突き詰めることはせず、なるべく少ないコストやリスクで出来るだけ大きいリターンを得たいためです。

さて、Dockerfileの改善をしたことのある方が読んでいるとしたら、おおよその原因や解決策がイメージできているのかなと思います。自分も事前にある程度イメージできてはいますが、昨今のAI活用の波に乗り、今回はせっかくなのでGitHub Copilot + GPT-5.2-Codex(以下、AI)の組み合わせで対応してみることにしました。

方針と効果検証

AIにやりたいことを伝えてやりとりした結果、以下の方針が提案されました。

方針 目的 効果 コスト リスク
A: マルチステージ ビルド成果物と実行環境の分離
B: slim ベース OS レイヤー削減
C: 依存最小化 Workspaces 範囲の縮小
D: キャッシュ削除 キャッシュ・不要ファイルの削除 小〜中

イメージサイズを削減する方法として、ざっくり4パターンあるということになります。ただ単に小さくするだけなら、これらすべてをやればいいだけなのですが、パターンごとにどれぐらい効果が異なるのかをAIに検証してもらうことにしました。
一言お願いするだけで検証用のDockerfileの作成から実行、その結果をドキュメントに書き出してくれるのはめちゃめちゃ楽ですね。

結果としてはおおよそ上記の想定通りで、AのマルチステージとCの依存最小化がそれぞれ50%程度の削減効果がありました。

ちなみにBのベースイメージをslimにする対応は、削減割合として低い結果となりました。これは元々のイメージサイズが5GBもあり、その大部分は不要なnode_modulesが占めていたためなので、通常は手っ取り早く出来てそこそこの効果が出る方法のはずです。

具体的な方法やテクニックは巷に溢れていると思うので割愛しますが、EventHubの場合はAIの提案した内容でほとんど問題なかったです。

最終結果と副次的効果

最終的にはすべてのパターンを組み合わせて対応することとしました。

その結果として、圧縮前で約5.0GBあったイメージが約0.7GBとなりました。

さらに小さくすることは出来そうですが、そのためにはパッケージマネージャの変更や更新、そもそも不要なパッケージの削除などなど、他の対応を行う必要がありそうでした。このあたりは実際に試していないためその限りではないかも知れませんが、前述の通りなるべく少ないコストで進めたいと考えていたので、別の機会で改善することにしました。

副次的な効果に関しては、docker buildとpushの時間が短縮されました。 依存関係を縮小することでbuildの時間(主にyarn installの時間)が減り、イメージサイズが小さくなったことで単純にpushの時間も減ったという感じです。時間にして合計およそ12分から3分への改善となりました。

Before:

After:

また、Batch処理のためにAWS Batch(with Fargate)を利用しているのですが、ジョブが作成されてから開始するまでの時間も短縮されていました。こちらは約3分20秒から約50秒になりました。その分、余計なコンピューティングリソースを使わなくなったので、インフラのコスト削減にも貢献できそうです。

そういえば「開発改善」って?

そういった背景や状況を踏まえ、顧客へ直接的な価値を提供する機能開発をやりつつも、「開発改善」を行う時間の割合を増やすことにしました。

タイトルや冒頭でぬるっと出てきた「開発改善」に関して補足させてください。

EventHubにおける開発改善とは「顧客へ直接的な価値を提供する機能開発」以外と表現してしまっていいかも知れません。

それらは一体どんなものなのか。
たとえば直近で実際に対応した/している内容で言えばこんな感じですね。

  • lint設定の改善
  • インフラのバージョンアップ対応
  • CI&CDの改善
  • 肥大化したDBのテーブルを分割

これらはサービスを継続的に改善・維持するために必要な活動です。
これまでにも同様のことは行ってきておりますが、これからはより明示的に時間を使っていくこととなり、またこれらの活動はいまいち効果の見えづらいものでもあります。

そのため、EventHubでは毎週のSprintレビュー時に開発改善に関する説明を行うこととなりました。

その際には、以下4つの価値指標で発表することとしています。

指標 具体的対象(非機能要件) ビジネス価値
1. 事業継続・防衛 セキュリティ、バックアップ、可用性、冗長化 情報漏洩やシステム停止などを防ぐため
2. ユーザー体験・信頼 応答速度、スケーラビリティ 「顧客離脱の防止と満足度」「重い」「落ちる」は即座に解約理由になります。快適な動作は、機能追加以上に顧客維持(リテンション)に直結します。
3. 変更の柔軟性 修正容易性(リファクタ)、テスト自動化、疎結合 「市場への適応速度」法改正やトレンド変化に合わせて、競合より1秒でも早く新機能をリリースできる「筋肉質な組織」を作ります。
4. 運用効率・コスト 監視、ログ整備、クラウドコスト最適化 「利益率の向上」属人的なトラブル対応時間を減らし、サーバー代を削減することで、営業利益を直接的に押し上げます。

 

先ほどのDockerイメージのサイズ削減の取り組みに当てはめた場合、EventHubでは以下のように捉えております。

  • 事業継続・防衛
    • 復旧時間の短縮
  • ユーザー体験・信頼
    • 特に変わりなし
  • 変更の柔軟性
    • 特に変わりなし
  • 運用効率・コスト
    • 開発の待ち時間短縮
    • インフラコストの削減

特に1日に何度も回しているCIのインフラコストが長い目で見て効いてきそうな気がします。

おわりに

今回は開発改善にさらに力を入れ始めたよ、というお話をさせていただきました。

改めて、開発改善はこれまでもやってきたことではありますが、セキュリティイシューの対策やEOLの対応など必要最低限のものに留め、機能拡充の優先度のほうが高かったことは事実です。

しかし、そのおかげもありEventHubというプロダクトは成長・拡大してきました。

そして今後の機能拡充の速度を落とさないようにしていくためにも、今一度土台を固めていくフェーズに差し掛かっている状態です。

そんなEventHubでは機能の開発だけでなく「開発改善」にも興味のあるフルサイクルなエンジニアを募集しておりますので、興味のある方はぜひ以下のリンクから覗いてみてください!

jobs.eventhub.co.jp

3度の改善を経て利用社数が急増したノーコード連携開発の裏側

はじめに

こんにちは!EventHub CTOの井関です。

これは EventHub Advent Calendar 2025 の24日目の記事です。

今回の記事では、今年2月にプレスリリースを出した「ノーコード連携」について、リリースまでの経緯とその後をまとめます。 実は、こうした連携機能は以前から提供していましたが、なかなか利用率が伸びず、3回の大きな改善を加えることでようやく今の形になりました。もちろん、プロダクトは常に進化しているため、現在の連携機能もまだまだ改善の途中です。

ノーコード連携は、EventHubが実現したい世界観の土台となる重要な機能です。そのため、「2月のリリースから9月末までの約7ヶ月間で、アクティブ利用社数を大幅に伸ばす」という高い目標を掲げていました。そして結果は……無事に目標としていた導入社数に到達し達成することができました!

この機能はお客さんの環境に合わせた設計が必要なため、セールス・カスタマーサクセス・カスタマーサポートといった様々な部署の協力なしには達成できませんでした。開発した機能が本当にお客さんに価値を届けるためには、全社的な協力がいかに重要かを再認識しました。

なぜ連携が重要なのか

EventHubは、出展、ウェビナー、小規模オフライン、オンラインカンファレンス、大規模オフラインまで、あらゆるイベントタイプに対応することで、イベントマーケティングの成果を最大化します。 成果を最大化するためには、単にイベントを開催するだけでは不十分です。開催データを可視化してPDCAを回すこと、そしてイベントで得た顧客データをその後の商談(CRM/SFA)にスムーズに紐づけることが重要になります。 だからこそ、EventHubの世界観を実現するために「データ連携」は欠かせない機能になります。

1回目のデータ連携:API公開

初期のデータ連携は、APIの公開から始まりました。この公開APIは現在も活用されており、ノーコード連携の礎となっています。 しかし、単にAPIを公開しただけでは、開発リソースを持たないお客様にとってはハードルが高く、商談データとして活用するまでには至りませんでした。結果として、利用率は低いままでした。

2回目のデータ連携:MA/SFA直接連携(ミニマム)

2回目は「商談での活用」に狙いを絞り、MA/SFA(Marketo, Salesforce, HubSpot)と直接連携できる機能を開発しました。 MA/SFA連携はできることが膨大で、全てに対応しようとすると開発工数が肥大化します。そのため、お客様側で仕組みを構築していただく「ミニマムな連携」を目指しました。 これにより利用率は向上しましたが、お客様に深いMA/SFAの知識が求められる仕様となってしまい、一部の方しか使いこなせないという新たな課題が生まれました。

3回目のデータ連携:柔軟性の追求

機能が限定されていることでお客様に高度な知識を求める形では利用促進が難しいため、お客様の利用用途に幅広く対応できるようにするためにより柔軟な仕組みを導入しました。ただ、今度は柔軟性を担保した影響でお客様自身が環境に合わせて設計と実装を行う必要が出てきてしまいました。柔軟性が高すぎたあまり、提案をしても「難しそう」と敬遠されてしまう結果となりました。

現在の「ノーコード連携」

そこで、実際に私自身も何度も商談に同席し、生のお客さんの声を拾い集めました。その結果、「ある程度の柔軟性を確保しつつ、EventHubが推奨する設計(ベストプラクティス)を簡単に取り込める方法」を模索し、リリースに至りました。 まだ課題はありますが、EventHub側での伴走が可能でありつつ、フルスクラッチの受託開発のようにはならない、「ちょうど良い塩梅」の形式に着地しました。これが功を奏し、利用率は飛躍的に伸びました。

最初のAPI公開(2023年1月)から約3年。この期間は、私たち開発チームにとって「何をもって完成とするか」の定義を問い直す時間でもありました。 最初は「連携機能が実装されていること」をゴールにしていましたが、それだけでは顧客の成果には繋がりませんでした。そこから3度の改善を経て、実際に商談成果が出るという状態まで解像度を高めたのが今回のノーコード連携です。 多くの利用実績が出た今、ようやくスタートラインに立てた気がしています。

得られたインサイト

今回の開発を通じて、いくつかの重要な学びがありました。

ヒアリングだけでなく「商談・オンボーディング」の現場に出る

「ご意見を聞かせてください」というヒアリングは色々な意見が聞けますが、そこで得られるものはどうしても「あってもいいかもね」といった温度感の内容も含まれます。 それよりも、「お客様がお金を払うかどうかの判断をする商談」に同席し、営業と共に提案する方が、シビアで本質的な情報を得られました。また、受注後もCS業務に入り込み、実際に課題が解決されたかを見届けることで、顧客解像度が劇的に上がりました。

既存だけでなく「新規」も見る

「利用率の改善」を目標にすると、母数の多い既存顧客にどうしても目が行きがちです。しかし、既存のお客様だけでは見えてこない視点や課題も多くあります。 これから運用を構築する新規のお客様の課題を理解しどのように解決するかも合わせて考慮しなければなりません。「誰にとっての価値か」を見誤らないよう、既存・新規のバランスを意識することが重要でした。

最初から「手離れ」を目指さない

SaaSプロダクトとして、お客様が自走できる(セルフオンボーディング)仕組みは必須です。しかし、最初からそこを目指してしまうと、顧客が躓く「本来の課題」を見逃してしまいます。 「技術的にできること」だけで自動化を進めると、「機能としては完成しているが、現場では使いづらい」プロダクトになりがちです。まずは手厚く伴走し、正解のフローが見えてからシステム化・効率化する順序が大切だと痛感しました。

インサイト以上に大切なこと

ここまでプロダクト開発の学びを書きましたが、今回高い目標を達成できた最大の要因は全社員がEventHubのバリューである「顧客を主語に」を体現してくれたことに尽きます。 実は今回の連携機能はセールス・カスタマーサクセス・カスタマーサポートにとっては、「負担」が増える側面もありました。

  • セールス: システム検証などの工程が増え、受注までのリードタイムが伸びる
  • カスタマーサクセス: MA/SFA環境に関する深い知識が必要になり、オンボーディングの工数が肥大化する
  • カスタマーサポート: 調査難易度が上がり、問い合わせ解決までの時間が増大する

自部門のKPIや効率だけを見れば、敬遠されてもおかしくない機能です。 しかし、それでもお客様の真の課題解決につながるならと部門の垣根を越えて一丸となり向き合ってくれました。適切な機能を作るためには前述の「インサイト」が重要ですが、その機能を真の価値としてお客様に届けるためには、全社的な連携が不可欠です。

改めて、今回の高い目標を達成できたのは様々な部署の協力があったからこそです。この場を借りて、全員に心から感謝します。

最後に

こうした形でエンジニアでも深く顧客を理解し必要な機能を考え抜いて開発を進めることをEventHubでは大事にしています。

even-eko.hatenablog.com

開発チームを拡大しようと考えた時からこの思想を打ち出して今の文化ができあがってます。顧客に向き合いながら開発することは時に大変ですが、大きなやりがいや事業成長に深く関わることができます。

ぜひ、そんな挑戦をしてみたいという方は連絡をお待ちしてます!!

jobs.eventhub.co.jp

次の25日目の記事は、弊社代表の山本です!! お楽しみに!

チーム開発の意思決定、どうすればスムーズにできる?

こんにちは、EventHubの井上です。昨年の12月から約1年ほどプロジェクトマネジメント※をやらせていただいてます。仕様の調整やテーブル設計などチームでの決め事をリードしなければならない場面が多かったのですが、なかなか苦労しました。

ちょっとした会議を進行するのも緊張しますし、何より会議の設計や進め方が未熟なために、議論が平行線になり、30分の予定が1時間経っても何も決まらない・・・なんてこともありました。

試行錯誤の日々でしたが、つい最近、上司から「会議の進め方が改善されてますね」とフィードバックをいただくことができました。

この記事では、周囲の方からのアドバイスや自身の経験を元に、チーム開発でスムーズに意思決定するためのtipsを5つご紹介します。

1つの事例として参考になる点があれば嬉しいです。

※ EventHubにはプロジェクトマネージャーという役職はなく、開発チーム内の任意のメンバーがその役割を担う体制になっています。

tips1: 提案ありきで受け手の負担を減らす

会議においてダメな方法は、「参加者に丸投げすること」です。自分の中でも整理できていないままダラダラと話して参加者の時間を消費し、結果、何も決まらなかったりします。

ではどのようにすると良いかというと、「提案とセットで相談すること」です。

例えばあなたが会議の参加者だとして、AさんとBさんがそれぞれ相談事を持ってきたとします。

Aさん:「〜〜なことで困ってまして、〜〜も調査したんですけどよく分からなくて、どうすればいいでしょうか?」

Bさん:「〜〜を解決したいと思ってます。原因は〜〜で、現状は〜〜なので、それを元にA~C案を持ってきました。〜〜な理由で、B案が良いと考えています。B案で進めていいでしょうか?」

どちらが答えやすいかは明白です。Aさんの相談には以下のような疑問が浮かび、参加者は何を答えればいいか頭を悩ませてしまいます。

  • そもそも何を解決したいんだろう?
  • 何をどこまで調査したの?
  • なんとなくこうするのがいい気がするけど、他にも良いやり方がありそうだな・・・。

一方で、Bさんの相談に対しては、それほど負担なく回答することができます。参加者は提案をベースに考えられるので、他の観点やより良い意見も出やすくなります。

提案をどう作るか

提案を準備するためのプロセスですが、自分はおおむね以下で進めています。

  1. 実現したいこと・目的を明確化する
  2. 現状どうなっているか調査し、整理する
  3. 確認すべきこと(= 論点)を洗い出す
  4. その中で重要な論点を絞る
  5. その論点について、複数案を用意して、自分なりの結論(= 提案)を決める

こう書くと難しく聞こえてしまいますが、要は、「相談する前にちゃんと調査して自分なりにどうすべきか考える」ということです。

以前の自分は次のような失敗をしていました。

  • 論点を絞らずに議論を進めてしまう。
    • 参加者は何を聞きたいのか、何を話せばいいかよく分からない。
      • 結果、何も決まらない。

ではどうすべきか。次のポイントを押さえると、決まりやすい会議になります。

  • 1回の会議で扱う論点は1つにする。
  • その論点について複数案を用意し、その中で1つ推奨案を提示する。

また、認識合わせや意思決定が必要な場面では、ドキュメント化して整理すると進めやすくなります。例えば、こんなフォーマットが役立ちます。

【背景】
- なぜこの議題が必要か?
- どのユーザー・どの機能に影響するか?

【現状と課題】
- 何が問題か?
- どの部分が曖昧か?

【論点】
- 今回決めたいことは何か?
- 仕様/実装/運用/UXのどれか?

【選択肢】
- 案A:
- 案B:
- ゼロ案(やらない選択肢):

【判断基準】
- 工数、UX、リスク、将来の変更コストなど

【推奨案】
- 私の提案はA案です(理由:●●)

【意思決定方式】
- 非同期でコメント募集(期限:●月●日●時)
- or 同期MTG実施(参加者:最小限●名)

【ログ】
- 決まったこと/決まらなかったことを残す

tips2: 論点を整理し、適切な人に相談する

何か困りごとがあった時に、相談相手を間違えてしまうことがあります。例えば、実装の制約についての困りごとをPdMやデザイナーに相談しても、「それは開発チーム内で決めてください」と返ってくるでしょう。一方で、どのような要件・仕様にすべきかを開発チームだけで決めてしまうのはよくないはずです。

なぜ相談相手を間違えてしまうのかというと、何を確認したいのか、自分の中で論点が整理できていないからです。tips1で紹介したようなプロセスで論点を整理しておけば、このようなミスコミュニケーションは防げるはずです。

自分の失敗例として、PdMに仕様のことを聞いたつもりが、実装観点の話が混ざってしまっており、「仕様観点か実装観点どちらの話ですか?」と聞き返されてしまったことがありました。これをきっかけに、論点を整理した上で、誰に何を聞くべきか意識してコミュニケーションをとるようになりました。

tips3: 図を活用する

提案書を作る時に、題材が複雑だと文章だけではうまく伝わらない場合があります。最近あった事例は、バッチ処理を行うタイミングをいつにするかの仕様を決める際、「現在時刻が〇〇で、設定時刻が〇〇の時、XX時に発火する」等と文章でまとめてみましたが、うまく伝わりませんでした。

そこで、Miroというツールで時系列を図表に起こしたところ、文章よりもずっと伝わりやすくなりました。自分自身も図に起こす過程で、見落としていたエッジケースに気づけたりと、思考の整理にもなると実感しました。

miroで作成した図

難点としては文章よりも手間と時間がかかるため、心理的に着手しづらいことです。正直に言うと、面倒なのでやりたくないと思ってしまいます。

面倒さを乗り越えるための心がけとして、以下を持つようにしています。

  • センスは不要。下手でも伝わればいい。
  • 最初は面倒なのは当たり前。
  • 皿洗いと同じでやり始めたら最後までやれる。

とは言え、文章よりも時間がかかることは事実なので、それほど複雑でない事柄は文章だけで十分だと思います。

生成AIで作る手間を省く

設計フェーズでよく使うER図、シーケンス図、状態遷移図などは、Mermaid記法で生成AI(Claude Code)に書かせ、Notion MCPでそのまま転記するような運用にすればそれほど手間をかけずに作成することができます。

また、近年はClaude CodeやCodexなどを使えば簡単なモックをすぐに作ることができるので、ローカル環境で動くものを作って共有すると、より共通認識を得やすくなると思います。

蛇足ですが、簡単なUIモックを作る時に、CursorのComposer 1に書かせてみると超高速で驚きました。公式によると、同等モデルの4倍の生成速度とのこと。一方で、より複雑なものを作る時は、Claude CodeやCodexの方が精度高くやってくれる印象です。

tips4: 期限を設定する

チーム開発で意思決定を行う上で、メンバー間の合意形成をすることが重要です。合意形成することでお互いに共通認識が持てますし、納得感のある状態で開発を進めることができます。

ただ、テーブル設計などではあるあるですが、それぞれ主義主張があるため、議論が紛糾し、なかなか決められないことがあります。また、設計書をSlackで共有したものの、誰もコメントしてくれない・・・なんてこともよくあります。

なぜこうなるのかというと、期限を決めていないからです。期限がないためにメンバーは以下のような心理になってしまいます。

  • まだ時間があるから、もう少し議論したい。
  • まだ時間があるから、あとでコメントしよう。

また、設計書をただ共有されただけだと、どうリアクションすればいいか分からず躊躇してしまうようなこともありえます。「どうコメントすればいいですか?」と聞いてもらうだけで解決しますが、起案者がそのあたりの案内をした方がスムーズに進むはずです。

以上を踏まえて、何かを提案するときは以下のように期限と決定方法を示します。

  • ◯月◯日の◯時に結論を決めます。
  • (簡単な事柄の場合)SlackのこのレスにYes/Noスタンプで投票してください。
  • (複雑な事柄の場合)設計書に疑問点など事前にコメントいただき、30分の会議で決めましょう。

多数決で意見が割れてしまったりしても、期限に達したら、提案者がベストだと思う案を選択し、他のメンバーは Disagree & Commit(賛同しないがコミットする) の精神で、チームでその案を進めていくことが大事です。

tips5: ログを残す

同じ議論を繰り返さないようにする、という観点で議論のプロセスや決定事項のログを残すことが重要です。

できれば決定事項は一箇所のマスタ文書に反映し、複数箇所に分散しないようにすると、後で混乱が生じるのを防げます。要件については要件書に、設計については設計書に更新を加え、常に最新の状態に保ちます。

議論プロセス(議事録)を残すことにも価値があります。例えば、別の開発者が後から改修を加える際に、「なんでこの設計になったんだろう。こうした方がいい気もするけど、何かこうしなければならない理由があったのかな」と疑問に思う場面があると思います。開発中のチームが実装時に「そういえばなんでこうしたんだっけ」と議事録を遡ることもよくあります。

EventHubではGoogle Meetを使用していますが、意思決定する会議では、録画と「Geminiによる自動メモ作成」機能を使い、負担なく議事録を残すようにしています。自動メモの精度は素晴らしく、要点が的確にまとまっていて、生成AIの進化を実感します。

まとめ

以上、5つのtipsをご紹介しました。チーム開発の経験が豊富な方は、無意識レベルでやっていることがほとんどだったかもしれません。こうして書いてみるとどれも当たり前のことだと感じるのですが、情報量が多く、タスクが山積みな状況ではつい疎かにしてしまうことがあるなと思いました。

抜け漏れがないように、以下のようなチェックリストをNotionテンプレートなどに貼っておくといいかもしれません。よかったら活用してみてください。

意思決定チェックリスト

- [ ] 目的は明確か?
 (なぜ決める必要があるのか、背景・課題が書けている)

- [ ] 相談相手は適切か? 
 (仕様→PdM/実装→開発/UX→デザイン etc)

- [ ] 論点は1つに絞れているか?
 (複数ある場合は順番に扱う or 別ミーティングに分ける)

- [ ] 選択肢(A案 / B案 / やらない案)は提示できているか?
 (推奨案と理由も示せているか)

- [ ] 図・モックなど、伝わる形に変換したか?
 (例:ER図 / シーケンス図 / 時系列表 / Before-After比較 / UIモック)

- [ ] 意思決定方法と期限は設定したか?
 (例:Slack投票・ドキュメントコメント・30min同期MTG)

- [ ] 決定後の次アクションが明確か?  
 (誰が何をいつやるか)

- [ ] 決めた内容を正しい場所に反映したか? 
 (例:要件書 / 設計書 / JIRAチケット / コーディング規約)

現在、EventHubではエンジニアを募集しています。本記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談からご連絡ください!

jobs.eventhub.co.jp

note.com

EventHub社内アンケートから見る、AI利用の現在地

はじめに

こんにちは、勝田です。

先日、社内で「Cursorの使い方とTips」に関する勉強会を開催しました。

そのときふと、「実際みんな、AIってどれくらい使ってるんだろう?」と気になって、エンジニアメンバーを対象に簡単なアンケートを取ってみました。

AIツールはもはや特別な存在ではなく、手元にあって当然の日常的な開発パートナーになりつつあります。

今回は、社内のリアルな声をもとに、AIとの向き合い方についてまとめてみました。

1. 普段使っているAIツール

EventHubでは、GeminiGitHub Copilot を契約しており、エンジニアがAIツールに触れるハードルは低く、日常的に活用されている環境が整っています。

 

実際に社内の声を聞いてみると、思った以上に複数のツールを状況に応じて使い分けている人が多く、「このケースならこのツール」といった選び方が自然に行われていました。

中でも興味深かったのは、Copilotに加えてCursorを積極的に選んでいる人が多かった点です。その理由としては、以下のような声がありました。

  • 補完の推測精度が高く、複数行や複数ファイルにまたがる補完が強力

  • プロジェクト全体を読み込んだうえで文脈に沿った提案をしてくれる

  • 操作が軽くストレスが少ない。反応も速い

 

ツール選定そのものがナレッジ化している今、「誰がどんな場面で何を使っているか」をオープンに共有し合える仕組みが、今後さらに重要になりそうだなと思いました。

2. サポートされている業務

コード補完、テスト作成、既存コードの意図の把握、ドキュメント生成、エラーの原因調査など。

「ゼロから創り出す」というよりは、既存の業務の加速や補助に活用しているというのが全体的な傾向でした。

僕自身も、まっさらな状態からAIに任せるより、ある程度書いたものに対して肉付けしてもらったり整理してもらったりするのが、一番フィットしていると感じています。

3. 生産性への貢献度

「非常に貢献している」「ある程度貢献している」という声が多く、補完を“切る理由がもはやない”と感じている人もいました。

実際、自分自身も最近はAIなしでコーディングするのが考えにくくなっていて、それくらい日々の開発に溶け込んでいる感覚があります。

ただし、「全部AIに聞けばOK」という状態では決してなくて、あくまで人間の判断があってこそ活きる道具という認識が、社内全体にしっかり共有されているのが印象的でした。

4. 印象的な成功体験

たとえば、

  • 「テストコード生成が圧倒的に速くなった」
  • 「SQLのパフォーマンスボトルネック調査で活躍した」
  • 「類似処理をテンプレ化して複製するのが爆速だった」

効率が上がっただけでなく、ストレスが減ったというニュアンスの回答が目立ちました。

とくに、「Cursorでコードの意図を要約してもらうと、他人のコードを理解するスピードが段違いだった」というコメントには、深く共感しました。

属人性を下げ、チームとしての可読性を底上げするツールになり得るという手応えを感じます。

5. 使いにくさや課題

  • プロンプト設計が難しい・面倒

  • 結局、自分で書いた方が早いこともある

  • 出力の正確性に不安がある

  • セキュリティ懸念(情報漏洩など)

このあたりは、自分もまさに日々感じているポイントです。

特に「正確な指示を出す」ための思考の流れは、これまでの“自分で手を動かして作る”スタイルとは明らかに違っていて、新しい思考回路が必要になる気がします。

結果として、「AIに聞く前にまず自分の思考を整理する」ことが求められるため、そこに負荷を感じることもあります。

いわゆるプロンプト疲れというやつですね。

6. ヒヤリハット体験

  • SQLにインジェクション対策が入っていなかった

  • ファイル構造が壊れそうになった

ツールが便利であるがゆえに、「どこまで信用していいのか?」という不安は常につきまといます。

だからこそ、信頼して任せる部分と、自分で必ず確認すべき部分の境界をどこに引くかが、エンジニアとしてのリテラシーになってきている気がします。

7. 今後AIの役割が増えることへの気持ち

「期待している」が多数派でしたが、同時に「正直不安もある」という声も一定数ありました。

ここでの不安は、AIの進化そのものよりも、自分がその変化に適応できるかという点にあるように思います。

キャッチアップのスピードに差が出やすいからこそ、学び方や使い方を支える設計がますます大切になっていきそうです。

8. 今後どう関わっていきたい?

積極的に関わりを増やしたい」「必要に応じて活用したい」という選択肢にすべての回答が集中しており、AIに対する前向きな姿勢が色濃く表れていました。

とはいえ、その前向きにもグラデーションがあって、日常的に使い倒していきたいという人もいれば、必要な場面で効果的に使えればいいという人もいます。

この違いはスキルやリテラシーの差というより、業務スタイルやタスク特性の違いによるものかもしれません。

だからこそ、誰もが同じように使いこなす必要はなくて、それぞれが自分にとってちょうどいい使い方を見つけていくことが、自然で無理のない関わり方なんだと思います。

9. その背景にある考えや期待、不安

  • 単純作業が減るのは歓迎

  • でも、間違いを見抜ける力も問われている

  • 「使える人」と「使えない人」の格差が広がりそう

  • ツールに振り回されたくはない

こうした声からは、AIへの期待と同じくらい、「本当にうまく使えるのか?」という戸惑いや、置いていかれることへの漠然とした不安も感じられました。

使えること自体をゴールにするのではなく、それをどう活かせるか/どこに効かせるかという視点のほうが、ずっと大事だと思います。

無理に使いこなすよりも、自分にとってのちょうどいい距離感や使いどころを見極めていくことが、これからのAIとの付き合い方の鍵になりそうです。

10. チームとして活用を進めるために必要なサポート

  • 社内Tipsの共有

  • 共通ツールの整備

  • 試せる余白

  • 各自の裁量を尊重する風土

今後も社内勉強会などを通じて、日々の開発に直結するナレッジをみんなで持ち寄りながら、よりよい活用環境をつくっていけたらと思っています。

このバランス感こそが、組織にとってのちょうどよいAIとの距離感を生み出していく鍵になるのではないでしょうか。

おわりに

AIツールが当たり前になった今、問われるのは「AIに何ができるか」ではなく、それをどんな文脈で、どう使いこなすか、どんなユースケースで価値を発揮できるかという視点です。

今回のアンケートを通じて、皆がとてつもないスピードで進むAIの進化にちょうどいい距離感で接しながら模索しているということでした。

 

その中で得た感覚や、工夫の共有が何よりのナレッジになると感じました。

こうしたナレッジが少しずつ社内にたまっていくことで、AIと協働しながら開発を進める精度やスピードが上がっていく可能性も、あると思ってます。

だからこそ、今回のようなアンケートや勉強会をきっかけに、無理なく自然にナレッジが循環する場を、今後も継続してつくっていけたらと思っています。

 

現在、EventHubではエンジニアを募集しています。本記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談からご連絡ください!

jobs.eventhub.co.jp

note.com

続・EventHubにおけるリリースフローの紹介

はじめに

こんにちは。EventHubの西内です。

前回はEventHubにおけるリリースフローの紹介というタイトルで記事を書かせていただきました。

tech.eventhub.jp

その際に「リリース後の話」や「開発着手前や着手中の細かい話」を話さないこととして挙げていたので、今回はそのあたりについて触れていければと思います。

また、EventHubではフルサイクルエンジニアを前提とした働き方を推進しており、いわゆる設計や実装およびテストといった一般的な開発業務以外で実際にどのようなことを行っているのかについてもお伝えしていければと思います。

リリース後の話

さて、リリース後の話についてですが、エンジニア採用資料のスライドから引用した図を元にすると”O&M”と”Support”が該当します。

O&M ( Operation & Maintenance )

いわゆる運用保守と呼ばれる活動を指します。組織によっては新規開発とは全く別の管轄になっているところもあるかも知れません。

EventHubだと以下のような活動例が含まれます。

  • 障害対応
  • インフラの監視や運用
  • 各種ライブラリ等のバージョンアップ

障害対応や監視を除けば、これらの対応を行うことが決定したら普段の開発スケジュールに乗ってくるので、特に難しく考える必要はないかと思います。

フルサイクルの文脈で言えば、自分たちで開発したプロダクトに責任を持ち運用することを踏まえて開発する必要がある、ということになります。「責任を持つ」と表現すると、プレッシャーを感じることになるかも知れませんが、その分だけ自由さを手に入れられることにもなると考えています。

たとえば、Aという設計や仕様だと運用が難しくなると思うなら、「A’という設計や仕様にしたい」と提案することができますし、EventHubはそういった提案への理解や尊重をしてくれる組織です。

Support

顧客や社内メンバー向けに、プロダクトに関してサポートする活動のことを指します。

O&Mと同様に活動例を載せると、以下になります。

  • 技術的な質問回答や調査
  • 不具合報告の一次受け

エンジニアがあらゆるサポートを行うかというとそういうわけではなく、EventHubにはCustomer Support(通称Cサポ)というポジションの方が数名在籍しており、社内外からの問い合わせに対して最初はCサポの方々に回答をしていただいています。そこからさらに技術的な調査や機能に関する詳細な振る舞いについての回答が必要な場合は開発組織に連携が行われます。

そうして連携された内容に関して、週替りの2名体制で対応する仕組みとなっています。

“技術的な質問回答や調査”では、先述の通りCサポだけでは回答が難しい技術的な質問やプロダクトの機能のエッジケースなどについて調査・確認を行い、結果をお伝えしています。

“不具合報告の一次受け”では、報告された内容を精査し不具合の重篤度を判断しています。このことをEventHubでは「バグレベル判定」と呼んでおり、判定のための目安になる定義も存在しています。また、同時に緊急度も判断し、必要に応じてエスカレーションを行うことがあります。

ちなみにこれらのやり取りはそれぞれ専用のSlackチャンネルがあり、Slackのワークフローを使って問い合わせや報告をテンプレートに沿って投稿できるようになっています。

問い合わせイメージ

その他

サイクルの図にはないですが他の活動で言えば、リリースした機能がどれぐらい利用されているかを不定期でチェックすることがあります。

このあたりはプロダクトマネジメントの文脈に当たるかとは思うので詳細は省きますが、こういった数値を調べることが顧客理解に繋がることがあると個人的には感じています。

開発着手前や着手中の細かい話

では次の話題に移ります。

前回、自分が記事を投稿したのは2024年の8月末頃でした。つまり、約8ヶ月ほど経過しています。

その間に、いくつかの記事がアドベントカレンダーやテックブログにて投稿されており、それらでも開発着手前や着手中の話について触れられているので、このタイミングで紹介しておきます。

もし良ければ以下の素敵な記事にも目を通していただければと思います!

note.com

note.com

tech.eventhub.jp

 

さて、まずは話の前提を揃えておければと思います。

  • ここで扱う”開発”とは?
    • お客様の要望に基づくスモールな開発(以下、スモール開発)
    • 今後のプロダクト戦略に沿った中・長期規模の開発(以下、エピック開発)
  • 開発フローの概要は?
    1. 要件定義
    2. 調査(Spike)&設計
    3. 実装&テスト
    4. リリース

EventHubではスクラムによるアジャイル開発を用いており、そしてどちらの“開発”についてもほぼ同じ開発フローで進め、規模感や要件に応じて適宜調整を行うイメージです。

スモール開発であれば、事前の調査や設計というフェーズを省いて実装しながら考えるということもありますし、事前にすり合わせてから実装に臨むこともあります。いずれにせよ、スケジュールは短めです。

エピック開発であれば、調査や設計に一定の時間を使うことが多いです。これは実現可能性の調査や後戻りできない(しづらい)設計にならないようにするためとなります。 また、実装とテストを進めていくうちに要件や仕様の調整や提案をエンジニアから持ちかけ、期日/品質/コスト/運用などのバランスを取りつつ、開発を進めていっています。

ここまでの話を聞くと「うちと大体一緒だな」と感じた方もいると思います。そしてその感覚はおそらく正しいと思います。大枠としては、他社と比較して特別な工夫をしているわけではないのかな、と感じています。

そのため、ここからは自分が感じたEventHubにおける開発組織の特徴を挙げる形で「開発着手前や着手中」の様子をお伝えできればと思います。

エンジニアからPdM/デザイナーに提案することがよくある

1つ目はこちら。

タイミングとしては随時発生する可能性があるものなのですが、実装に着手し始めてからが多い気がします。開発が進むにつれて解像度が上がってきたり、UIを実装して動くものが出来てきたりするからかなと思っています。

提案する際の観点としては、より良い機能にするためであったり、実装コストを抑えて素早くリリースするためであったりします。他の観点もあるとは思いますが、最たる例はこの2つだと感じます。

具体例としては、仕様に関して「なぜAという仕様ではなくBなのか」みたいな疑問を感じて質問や相談をすることがあるかと思います。そういった場合にはAやBの仕様のメリットとデメリットを考えたうえで、提案しつつ質問や相談することをEventHubでは推奨しています。

可能であれば提案ベースで話すことを推奨しているので「提案することがよくある」のは至極当然の話ではあるのですが、自分がこれまで在籍した3社と比較しても多いほうなんじゃないかと感じています。

いまやる必要があるのか?の判断軸での議論がよくある

2つ目はこちら。

たとえばPull Requestの内容として、本来のタスクでやりたかったことに加えてリファクタを含んでいたとします。そのリファクタについて「いまやる必要があるかどうか」の軸でレビュイーが意義を説いたりレビュアーが意見したりする、そういった場面を目にすることがあります。

言わずもがなですが、リファクタ自体を否定しているわけではなく、むしろ推奨しているぐらいではあります。しかし、EventHubではそれ以上にタイミングを大事にしているということかなと個人的には感じています。

「リファクタリングをしていてリリースが遅れた」となれば、顧客への価値提供がその分遅れることになります。反対に「今後の開発速度に影響を与えかねないから、いまリファクタリングをする」という判断をすることもあります。

だからこそ”いまやる必要があるのか?”を問う必要があるのだと思っています。

さいごに

けっこう具体例を盛り込んでみたのですが、いかがだったでしょうか。前回同様、開発組織の雰囲気が少しでも伝わっていれば良いなと思います。

あと、リリースフローの紹介というタイトルでしたが「EventHubにおけるフルサイクルエンジニアはどういったことをしているのか?」みたいなタイトルでもあんまり違和感がない話になったな、と書いた内容を振り返ってみて感じました。

…というわけで、ここまで読んでくださりありがとうございました!
いつもの採用情報などを記載しておいて本記事を締めさせていただきます。

jobs.eventhub.co.jp

note.com

初めてのプロジェクトマネジメントで学んだ3つのこと

こんにちは、EventHubの井上です。2024年12月から2025年3月の約3ヶ月に渡り、エピック(リリースまで数ヶ月を要する中規模以上の機能開発)のプロジェクトマネジメントを担いました。これまでマネジメント経験がない中での挑戦だったため、いくつもの壁にぶち当たりました。本記事では、プロジェクトを通して学んだ3つのことをご紹介します。

開発体制について

本題に入る前に、開発体制についてご紹介します。今回のエピックは以下のメンバー構成でスクラムによる開発を行いました。

  • プロダクトオーナー(PO)2名:メインPOとサブPO
  • デザイナー1名
  • エンジニア4名
    • チームリーダー1名
    • メンバー3名(私を含む)

上記エンジニア4名は普段から固定のチームで動いています。プロジェクトマネジメントは「エピック担当」という名称で、チームリーダーを含む人員の中から一人がエピックごとに交代制で役割を担います。今回は私がエピック担当として選出されました。

1. プロジェクトの完了に責任を持つ

マネジメントという言葉から、なんとなく「管理する」のが仕事だと思っていましたが、プロジェクトの完了に責任を持つのが仕事だと教わりました。メンバーの協力を得たり、自分自身でも手を動かしたりして、あらゆる手段を使って完了させる意識で取り組みました。

特に意識していたことは以下2点です。

  • 自身のコミットしたタスクは必ず期限内に終わらせること
  • 全てのPRに目を通し、In Reviewになった当日にレビューすること

EventHubでは1週間スプリントのスクラム開発を行っていて、JIRA上のスプリントに乗せた全てのチケットをDONEにするために、とにかく実装とレビューを頑張りました。メンバーからの協力を得るために、自分は本気でこのプロジェクトを完了させたいんだという姿勢を示すようにしました。

また、レビューサイクルを早めることでチーム全体の実装スピードが上がると感じたため、認知負荷はかかりますが、GitHubやSlackの通知がきたらすぐにレビューするようにしました。

少し根性論のようなやり方かもしれませんが、マネジメントの引き出しのない自分が小手先であれこれやるよりも、自分が誰よりもプロジェクトにコミットするのが、シンプルで効果的だと考えました。

2. 見積もりに時間をかけすぎない

今回のエピックは機能スコープが広く、設計やチケットの洗い出しに苦労しました。普段のエピックでは技術スパイクという形でプロトタイプ的なものを作り、それを元に精度の高い見積もりを行うようにしていますが、全てをカバーしたプロトタイプを作るのは現実的ではありませんでした。

そこで、機能全体に関わる部分だけスパイクを行い、その他は解像度の粗い状態でチケットを切りました。その後のチームでの見積もりも当然ざっくりしたものになりますが、各チケットの担当者が実装する中で精度を高めていった方が、結果的に早くリリースできると考えました。

副作用として、8ptと見積もっていたものが実際は21ptぐらいのサイズだったことが判明したりと、当初よりポイントが膨らんでしまうものもありましたが、不確実性の高いものから取り組むようにしたことで、大幅なスケジュール遅延は避けられました。この点については後述します。

粗い見積もりでプロジェクトをスタートすることにはリスクが伴いますが、実装してみないと分からないことも多いため、早めに実装フェーズに移ることで、結果的にリリースタイミングを早めることができたと思います。例えば、1ヶ月かけて精緻に見積もりし、2ヶ月半のスケジュールを引いてぴったり完了したらトータル3ヶ月半です。一方で、2週間で粗い見積もりをし、2ヶ月のスケジュールを引いて2週間の遅れで完了したら3ヶ月でリリースできたことになります。

ただ、計画より遅延すると今後の開発計画やユーザーコミュニケーションに問題が生じる可能性があります。そのリスクを抑えるために、序盤は仮スケジュールと周知しておき、エピックの中盤でスケジュールを確定するような形にするといいのかなと思います。実際、今回のエピックも「見積もりが粗いので仮スケジュールです」と関係者にお伝えしてから開始しました。

3. 不確実性の高いものから取り組む

見積もりの話と関連しますが、工数やスケジュールの確度を上げるためには、不確実性の高いタスクから取り組むと良いことを学びました。個々のチケットの不確実性の総和が、リリーススケジュールの不確実性だとすると、当然、不確実性の高いものから順に取り組めば、後に残るのは工数が読めているものばかりになるため、スケジュールが予測しやすくなります。何より心理的な面で安心です。

ただし、不確実性の高さよりも優先すべきは、チケット単体でユーザーに価値提供できるものです。結果的に同じ工数なら、ユーザーへの提供は早いに越したことはないからです。また、DBマイグレーションなど先に終わらせておくことで後続のタスクがスムーズに進むものは、並列でタスク消化できるように優先すべきです。

以上を踏まえ、今回のエピックでは以下の優先順位でチケットを配置しました。

  1. ユーザーに価値提供できるストーリー
  2. 複数のブロッキングになるタスク
  3. 不確実性の高いタスク
  4. その他のタスク

不確実性の高いタスクは実装方針が不明瞭なことも多く、難易度が高いです。そのため、着手することに不安を覚えます。「どのチケットやりたいですか?」という希望性で取っていくと、たいてい不確実性の高いタスクは敬遠されて後ろ倒しになってしまいます。なので、不確実性の高いことから取り組む重要性をチームで共有し、勇気を持って先に着手していくことが大事だと思います。私個人としては、少なくとも選り好みはしないように気をつけました。

余談ですが、今回のエピックでは、序盤に不確実ではないと判断していたものの、後半になって考慮漏れがあったことに気づき、大幅な作業量増になってしまったチケットがありました。この問題の原因や防止策はチームで改めて振り返る予定ですが、目の前の実装に集中するあまり、チケットの再整理に時間を割けていなかったのが個人的な反省です。このあたり、自身の技術力やマネジメントスキルの不足であると実感します。

さいごに

今回のエピックは3月末で一旦終了となる予定ですが、来月すぐに後続のエピックが控えていて、そのマネジメントも引き続き担当します。きっと最適なプロジェクトマネジメントの仕方があるのだと思いますが、たった数ヶ月で熟練することはできなさそうです。今回と同じく個人としてはフルコミットでやりつつ、周囲の方の助けをお借りして、完了責任を果たそうと思います。

初めて指揮役をやってみて気づいたのは、チームの中にプロジェクトにフルコミットしてくれる方がいるととても心強いということです。自分自身もメンバーとして入る際は、今まで以上に前のめりな姿勢でやろうと思っています。プロジェクトマネジメントはなかなかタフな仕事ではありますが、個人の成長という意味でもすごく価値のある経験でした。3ヶ月間、未熟なマネジメントで課題だらけの中、一緒に走りきってくれた皆さんに心から感謝しています。

現在、EventHubではエンジニアを募集しています。本記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談からご連絡ください!

jobs.eventhub.co.jp

note.com