テーブル初期設計のポイント:事実(Fact)を元に設計する

アプリケーションにおいてのコアはデータです。
データ設計に問題があるアプリケーションは、意図がわかりづらく、実装が複雑になり、その結果、不具合が発生しやすくなります。また、後からデータ設計を変更することはコストが大きく、簡単に変更もできません。

この記事では、データベースの初期設計において考慮しておくべきポイントと実務で使いやすい方法について解説します。

テーブル初期設計の基本ポイント

テーブル設計において、特に「状態を持つデータ」の設計は難しいものです。
ユーザー情報を例にすると、status カラムに active / inactive / banned などの状態を持つという設計はよくある設計でしょう。このように状態を持つ設計は一見シンプルに見えますが、実は後々問題になりがちです。

なぜ状態を持つ設計が問題になりやすいのか

状態を1つのカラムで管理する設計は、一見シンプルに見えますが、実際には以下のような問題が発生しやすくなります。

データの不整合が発生しやすい

状態を持つとデータの不整合が発生しやすくなります。

1
2
3
4
5
6
-- 悪い例:派生情報をカラムに保存
users
- id
- email
- is_active -- 派生情報
- deleted_at -- 事実

この設計では、deleted_at が設定されているのに is_active = true のまま、という矛盾した状態が発生する可能性があります。

データが曖昧になりやすい

初期設計では active / inactive の2つだった状態が、後から以下のような状態が必要になることがあります。

  • banned (アカウント停止)
  • suspended(一時停止)
  • pending_verification(認証待ち)
  • trial_expired(トライアル期限切れ)

このように後から状態を追加していくと、いつの間にか複数の意味を持つステータスが1つのカラムに入っているということが発生します。そうなると、カラムの意味がぼやけてしまい、明確なデータ表現が難しくなってしまいます。

テーブル設計の原則

事実(Fact)だけを保存する

テーブル設計の原則は 業務・ドメインの事実(Fact)だけを保存する です。

  • 事実:登録された、退会した、BANされた、メール認証した
  • 派生:有効ユーザーかどうか、ログイン可能かどうか

派生情報は保存せずに事実から算出します
このように、事実のみを保存するという原則に則ることにより、データの不整合や曖昧さに強い設計にすることができます。

以下は、ユーザーの情報を日付型として扱う例です。1つのカラムでは1つの意味を取り扱うことで、明確なデータ設計にしているのがポイントです。

1
2
3
4
5
6
7
8
9
-- 良い例:日時型を使う(状態が増えたらカラム追加で対応)
users
- id
- email
- deleted_at -- 退会日時
- banned_at -- BAN日時
- suspended_at -- 一時停止日時
- email_verified_at -- メール認証日時
-- 新しい状態が増えても、新しい日時カラムを追加するだけ

状態=「事実の集合」として考える

状態は事実の組み合わせです。例えば、ユーザーが「利用可能」かどうかは、これらの事実の組み合わせから判断します。

  • 退会していない
  • BANされていない
  • メール認証済み
1
2
3
4
-- 利用可能ユーザー
WHERE deleted_at IS NULL
AND banned_at IS NULL
AND email_verified_at IS NOT NULL

ユーザーが「利用可能」というような状態は事実を元に算出する設計が安定します。

状態として持つ場合

一方で 「割り切って状態を持たせた方が“良い設計”になるケース」 は、実務では普通にあります。

UI / 管理画面で即座に使いたいとき

例:決済管理画面

例えば、一覧で「未払い」「支払済み」「返金済み」を表示したい場合は、状態を持ったほうがよいことがあります。
一覧画面で毎回集計の計算をするのはパフォーマンスが重くなりがちです。そういった場合は割り切って状態を持ったほうがトータルとして安定します。

1
2
3
4
invoices
- id
- total_amount
- status -- unpaid / paid

ルール

状態を持つときは以下のルールを守るのがポイントです。

  • 状態が事実から再計算できる

ワークフローが明確に有限なとき

例:審査フロー

審査フローのようにワークフローが明確で有限な場合は状態として持って問題ありません。

1
draft → submitted → reviewing → approved / rejected

なぜOKか

  • 状態遷移が業務ルールとして固定
  • 「今どこか」が重要

一生変わらない意味の状態

状態の意味がが変わらない場合は状態として持って問題ありません。例えば、次のような場合です。

  • is_admin(管理者かどうか)
  • is_trial_user(トライアルユーザーかどうか)
  • is_archived(アーカイブされているかどうか)

補足:YES/NOで判断できるという意味?

多くの場合は YES/NO(boolean)で表現できる ということです。ただし重要なのは、単に2値で表現できることではなく、次の2点です。

  • 定義が将来もブレない:運用や要件が変わっても「何を意味するか」が変わらない
  • 派生状態ではない:他の複数の事実の組み合わせ(計算結果)になっていない

例えば is_admin は「管理者権限が付与されているか」という 明確な事実 に近いのでフラグとして持ちやすいです。一方で is_active は「退会していない」「BANされていない」「認証済み」など条件が増えやすい 派生状態 なので、フラグとして持つと破綻しやすくなります。

まとめ

「状態」を保存するのではなく、まずは「事実(Fact)」を保存するのが、拡張と運用に強い設計です。

  • 原則: DBには 事実(イベント・日時) を保存し、状態は 計算結果 として扱う
  • 避けたいこと: status のような1カラムに、意味の違う状態を押し込める(曖昧さ・不整合の原因)
  • 例外(状態を持ってもよい): UI/管理画面で即時に必要、ワークフローが有限で明確(例:請求ステータス、審査フロー)など、割り切る理由がある
  • 状態を持つときのルール: 再計算できる / 壊れても直せる(できないなら、まずFactの持ち方を見直す)

テーブルは「状態を持つ存在」ではなく、「状態を生む出来事を持つ存在」 として設計することがポイントです。

「社員と業務委託を区別しない」は法的リスクのサイン

はじめに

「うちは社員も業務委託も区別しません」

スタートアップやベンチャーの現場で、こうした言葉を聞いたことがある人も多いのではないでしょうか。

一見するとフラットで良さそうに聞こえますが、法的・構造的には実は危ないサインです。

本記事では、

  • なぜ社員と業務委託を同じように扱えないのか
  • 同じ扱いをすると何が起きるのか
  • 具体的な事例
  • 経営者・リーダー・フリーランスがそれぞれ注意すべきポイント

を、実務目線で整理します。


結論:社員と業務委託は「同じように扱えない」

結論から言うと、社員と業務委託は同じようには扱えません。

理由はシンプルで、日本の法制度では次のように位置づけられているからです。

  • 社員(雇用):労働力を提供する人(民法第623条「雇用」、労働基準法第9条「労働者」)
  • 業務委託:成果を提供する事業者(民法第632条「請負」、第656条「準委任」)

このように法律として区別されているため、社員と業務委託は同じように扱うことができません。

業務委託とは何か?

会社が社外のフリーランスに業務を委託する際の契約が業務委託で、成果物や業務の遂行に対して報酬が支払われます
業務委託には、請負契約準委任契約の2種類がありますが、どちらも以下の特徴があります。

  • 裁量の委譲:作業の方法や手順は受託者が決定する
  • 時間の自由:作業時間や場所は受託者が決定する
  • 独立性:受託者は独立した事業者として業務を遂行する
  • 指揮命令権がない:クライアントは作業の方法や手順を指示できない

雇用契約との違い

雇用契約と業務委託契約は、法的に全く異なる性質を持ちます。

雇用契約の特徴

雇用契約は、労働力を提供する契約です。

  • 労働力の提供:時間と労力を提供することで報酬を受け取る
  • 指揮命令権:使用者(会社)が労働者に対して業務の内容や方法を指示できる
  • 時間の拘束:就業時間や場所が指定される
  • リスクの所在:使用者が業務のリスクを負う

なぜ区別が重要なのか

この区別が曖昧になると、偽装請負・偽装委任とみなされる可能性があります。

偽装請負・偽装委任とは、実質的には雇用関係にあるのに、業務委託契約(請負契約や準委任契約)の形式を取ることで、労働法上の義務(社会保険、残業代など)を回避しようとする行為です。

社員にできて、業務委託にできないこと

社員にだけ許されている管理(指揮命令権)

指揮命令権とは、次のような権限のことです。

  • いつ働くかを決める
  • どこで働くかを決める
  • どの順番で、どの方法で作業するかを指示する
  • 業務の進め方を細かく統制する
  • 従わなければ不利益を与える(評価・報酬・契約継続など)

これは 労働契約(雇用)にだけ認められている権限 です。
こういったことを業務委託に行うのは法律的にNGです。

問題になりやすいケース

社員と業務委託の区別をしていない現場でありがちな、問題になりやすいケースです。

1. フラットなことが良い文化だと思っている

「区別しない=対等」という認識は一見よさそうに思えますが、そもそも、法律の扱いが違うため、同じ扱いはできません。法律に則った対応をする必要があります。

2. 管理職が雇用しか知らない

管理職に法的な知識がなく、業務委託を社員と同じように管理することに問題意識がない場合があります。

3. 社員も業務委託も同じように扱いたい

現場でよくある本音です。

  • 業務委託でも一緒にSlackにいてほしい
  • 社員と同じスピード感で動いてほしい
  • 社員と同じように働いてほしい

これは全部、雇用の安心感を委託で代替しようとしている状態です。雇用関係の契約をするのが本来あるべき姿です。

具体事例

エンジニアの現場でありがちな具体的なケースを例に説明します。

法的リスクが高いケース

以下のケースは、明らかに雇用関係とみなされる可能性が高く、法的リスクが高いケースです。

ケース1:出退勤の管理を求められる

1
2
「業務委託でも、毎日9時から18時までSlackにいてください。
離席する時は必ず報告してください。」

問題点:勤務時間の拘束と離席連絡の義務は、指揮命令権の行使です。業務委託にこれを求めることは、雇用関係とみなされる可能性が極めて高いです。

ケース2:作業方法を細かく指示される

① 実装手順をステップ単位で指定される場合

1
2
3
4
まずこのAPIを呼んで
次にこのstateに入れて
useEffectはここで使って
この条件分岐で描画を切り替えてください

② レビューが「やり方の矯正」になっている場合

1
「この書き方は違います。このように直してください」

問題点:作業の方法や手順を細かく指示することは、指揮命令権の行使です。業務委託では、作業の方法は受託者が決定します。

ケース3:即レス文化を強制される

1
2
「業務委託でも、Slackのメッセージには
1時間以内に返信してください。」

問題点:常時オンラインを前提とした即レス文化は、時間の拘束にあたります。業務委託では、作業時間や場所は受託者が決定します。

ケース4:ビデオチャットでの常時接続を要求される

1
2
「勤務中はビデオチャットで常時接続しておく必要があります。
作業中は常にカメラをオンにしておいてください。」

問題点:勤務時間中にビデオチャットで常時接続を要求することは、時間の拘束場所の拘束の両方に該当します。業務委託では、作業時間や場所は受託者が決定します。

グレーなケース

以下のケースは、境界線上で、実態によっては雇用関係とみなされる可能性があります。

ケース1:定期的なミーティングへの参加

1
2
「毎週月曜の10時から定例ミーティングがあります。
業務委託の方も参加してください。」

判断基準

  • 問題あり:参加が必須で、欠席すると評価に影響する場合
  • 問題なし:参加は任意で、成果物の確認や情報共有が目的の場合

ケース2:コードレビューの指摘

1
「この実装方法はパフォーマンスに問題があるので、こちらの方法に変更してください。」

判断基準

  • 問題あり:実装方法を強制し、従わないとレビューをOKとしない場合
  • 問題なし:技術的なアドバイスや改善提案として提示される場合

ケース3:納期の設定

1
「この機能は来週金曜までに完成させてください。」

判断基準

  • 問題あり:納期が短すぎて、実質的に作業時間が拘束される場合
  • 問題なし:成果物の納期として、合理的な期間が設定されている場合

ケース4:Slackでのコミュニケーション

1
「業務委託の方も、社員と同じようにSlackでコミュニケーションしてください。」

判断基準

  • 問題あり:常時オンラインを前提とし、即レスを強制する場合
  • 問題なし:成果物の確認や情報共有のためのコミュニケーションツールとして使用する場合

ケース5:稼働時間

1
「準委任契約で、月に160時間稼働していただきます。」

判断基準

  • 問題あり:毎月必ず160時間働くことを義務付けている場合(時間の拘束)
    • 例:「月160時間の稼働を必須とします」「160時間未満の場合は報酬を減額します」
  • 問題なし:月160時間を想定しているが、時間の拘束はなく、実際に行った事務処理の時間に対して報酬が支払われる場合
    • 例:「月160時間程度を想定していますが、作業時間や場所はお任せします。実際に作業を行った時間に対して報酬を支払います」

重要なポイント

  • 準委任契約で時給ベースの報酬設定は法的に可能です
  • しかし、「月にxxx時間働くことを必須とする」という時間の拘束は、雇用関係とみなされる可能性が高いです
  • 実際に行った作業の時間に対して報酬が支払われる場合は、問題ありません

問題なし

以下のケースは、業務委託として適切な運用です。

ケース1:成果物ベースの評価

1
2
「この機能の実装をお願いします。
完成したら、コードレビューをして納品してください。」

適切な理由:成果物の完成を目的としており、作業の方法は受託者が決定します。

ケース2:技術的なアドバイスの提供

1
2
「この機能は、パフォーマンスを考慮するとこちらの実装方法がおすすめです。
ただし、最終的な判断はお任せします。」

適切な理由:技術的なアドバイスは提供していますが、最終的な判断は受託者に委ねられています。

ケース3:成果物の確認とフィードバック

1
2
「完成した成果物を確認しました。
この部分は要件と異なるので、修正をお願いします。」

適切な理由:成果物の確認とフィードバックは、業務委託契約の範囲内です。ただし、修正の方法は受託者が決定します。

ケース4:情報共有のためのコミュニケーション

1
2
3
「プロジェクトの進捗を共有するため、
週1回、30分程度のミーティングを設定しています。
参加は任意です。」

適切な理由:情報共有が目的で、参加が任意であれば、時間の拘束にはあたりません。

ケース5:成果物の納期設定

1
2
「この機能の実装をお願いします。
納期は2週間後を想定していますが、作業の進め方はお任せします。」

適切な理由:成果物の納期として合理的な期間が設定されており、作業の方法は受託者が決定します。

経営者・リーダーに伝えたいこと

もし、

  • 常に一緒に動いてほしい
  • 時間帯を揃えたい
  • 社員と同じように働いてほしい

のであれば、業務委託ではなく雇用を選ぶべきです。

業務委託は、

  • 裁量を渡す
  • 成果で評価する

という覚悟が必要な契約形態です。会社の都合で契約を終了できる社員ではありません

同じように扱った場合に会社が被るリスク

現実的なリスクは次の通りです。

  • 労基署からの是正勧告
  • 残業代・社会保険の遡及請求
  • 外注費の税務否認(業務委託として計上した外注費が、実態が雇用関係と判断され、給与として計上し直すよう指摘される)
  • IPO・M&A時の重大指摘

実際に厚生労働省はフリーランスが実態は労働者として働いていないかの確認を強化する方針を掲げており、全国の労基署に相談窓口が設置されています。

「労働者性に疑義がある方の労働基準法等違反相談窓口」を労働基準監督署に設置します

フリーランスがクライアントを選別するために

健全なクライアントは、次のように答えられます。

  • 「勤怠管理はしません」
  • 「成果物ベースでお願いします」
  • 「やり方はお任せします」

社員と同じようにやっていただきたいという考え方の会社の場合、業務委託は長期的には消耗戦になりがちです。

正社員脳 → 業務委託脳 への切り替え

長年正社員で経験を積んできたエンジニアにとっては、勤怠管理は当たり前の概念です。私もそうでした。
しかし、正社員と業務委託では法的に区別されるため、考え方を切り替える必要があります。

整理すると、こうです。

正社員の世界

  • 時間を会社に提供する
  • 管理されるのが前提
  • 出勤・退勤が価値の測定単位

業務委託の世界

  • 専門性・判断・作業を提供する
  • 管理されないのが前提
  • 時間は 計測するが、拘束されない

おわりに

「社員と業務委託を区別しない」という言葉は、善意であっても、無知であっても、リスクのサインです。
契約形態を正しく理解し、お互いが健全に協力できる関係を構築することが、結果的にプロダクトにも組織にもプラスになります。

この記事が、

  • 経営者・リーダーにとっては運用を見直すきっかけに
  • フリーランスにとってはクライアント選別の軸

になれば幸いです。

エンジニアの退職は「人の問題」ではなく「意思決定プロセスの問題」

近年、プロダクト部門はビジネスの成果を左右する重要な部門となっており、その組織を構成するエンジニアの質がビジネスの成果に大きく影響するようになっています。一方でエンジニアの離職率の高さと採用の難しさが経営の問題となっている組織も少なくありません。

この記事では、なぜエンジニアがやめるのかをエンジニアの心理と構造の点から解説し、組織がとるべき対策について論じます。

1.退職の理由

まず前提として、エンジニアはその会社をできる限り続けたいと考えています。
なぜなら、転職にはリスクを伴うからです。新しい組織に移動すると今までの人間関係がリセットされ、ゼロから関係を築いていく必要があります。そういった苦労はできれば避けたいと考えています。

それなのに、なぜやめてしまうのでしょうか。給料が安い、残業が多い、技術的な学びが少ない、福利厚生が乏しい。どれもそれらしい理由に見えますが、実はこういった問題は本質的な問題ではありません。

エンジニアが退職する根本的な理由は、その会社や組織がどうにもならないと判断したからです。多くのエンジニアは退職以外に現状を改善する手段がなくなったため、退職しているのです。

2.静かな危険信号

以前は意見を言っていたのに、最近静かになったと感じる人はいませんか?最も退職のリスクが高い人は最近静かになった人です。

文句・反論・衝突は「改善の余地がある」というサインです。声を上げている人は、まだ組織に期待をしている状態です。その期待がなくなった時、不満を表に出さなくなります。

沈黙は判断の結果

エンジニアが不満を表に出さない理由は、単に我慢しているからではありません。声を上げるコストと、それによって得られる利益を天秤にかけた結果、声を上げることをやめているのです。黙るのは「合理的撤退判断」です。

論理的思考の強いエンジニアは、感情的な反応ではなく、冷静な判断に基づいて行動します。組織が変わる可能性が低いと判断した時点で、声を上げることをやめ、次のステップを考え始めます。

3.なぜ組織は「沈黙」を危険信号として認識できないのか

このように「沈黙」は危険信号ですが、多くの組織は危険信号として認識することができていません。これは沈黙を計測するのが難しいという構造上の理由からです。

表面上は何も問題が起きていないように見える

納期は守られています。いつもと変わらず仕事は進んでいるように見えます。表面上は何も問題が起きていないように見えるため、組織は沈黙を危険信号として認識できません。

マネジメントが見ている指標がズレている

マネジメントは数値化できる指標、例えば、アウトプット量・稼働率を見ています。意思決定の質については計測することが難しく、現場レベルでの感覚でしかわからないことも多いです。

このズレが、沈黙を見逃す原因となっています。沈黙はKPIに現れません。エンジニアがやめる時、組織は初めて問題に気づくのです。

4.問題は「意思決定プロセス」

声を上げることをやめる原因の多くは、「意思決定プロセス」にあります。反対意見を言うことにリスクがあったり意味を持たない状態が続くと、エンジニアは意見を言うことをやめます。例えば、

  • 反対意見を言ってもその場で突っぱねられる
  • 意見の違いに寛容ではない(意見を言うこと自体がリスクになる)
  • その場では検討しますと言われるが、実際は検討されない

意見 → 何も変わらない、の繰り返しが続くと、発言コストだけが蓄積していきます。この状態が続くと、エンジニアは「この組織では言っても無駄だ」と判断し、意見を言うことをやめます。

以下は私が実際に観察した問題のある意思決定プロセスです。

ファシリテーターが意見を評価する

ある開発チームの会議で、開発チームのリーダーが会議をファシリテーションしたのですが、そのファシリテーションに問題がありました。

開発チームのリーダーはメンバーが意見を言ったその場でその意見を評価し、その内容からはそれはちょっと違うというようなニュアンスが感じられました。そのような反応をされてしまうと、メンバーは正直に自分の意見を言うことが難しくなります。

ファシリテーターは参加者の発言を促し、会議を前に進めることが目的です。ファシリテーター自身は意見を評価するべきではありません。ファシリテーターが意見を評価すると、ファシリテーターの顔色を伺いながらメンバーが発言をすることになり、意見を言うことを躊躇したり、意見を忖度することになるからです。

このように誰かの顔色を伺いながら発言しないといけない状態は、意思決定プロセスに問題があると言えるでしょう。

意見の違いに寛容ではない(意見を言うこと自体がリスクになる)

意見の違いに寛容でないことは長期的にリスクがあります。短期的には意思決定の速度が上がりますが、メンバーが考えることをやめてしまい、長期的には組織としての意思決定力が弱くなってしまうからです。

このような事例がありました。

ある会議で開発者の一人がシステム的な問題について意見を述べているところ、リーダーがその話を遮り、問題は別にあるということを話し始めました。話を遮られたメンバーは自分の意見が尊重されていないと感じるでしょう。

リーダーとメンバーのような構造はリーダーの意見が構造上強くなります。リーダーがメンバーの人事権を持っていることが多いためです。
リーダーは組織の構造的な力学を理解し、幅広い意見を収集するという意識を持つ必要があります。しかし、そのような視点を持っているリーダーは多くありません。特にプレイヤーとして優秀だった人がリーダーになった場合は、自分の意見が正しいという意識が強い傾向にあります。

本来は様々な観点から問題を分析する必要がありますが、誰かと異なる観点で発言をした時に話を遮られてしまうような状態は、意思決定プロセスに問題があると言えるでしょう。

5.沈黙の代償は、想像よりも高い

意思決定プロセスのまずさは次のような問題を引き起こします。

エンジニアの退職

黙っている人はある日突然やめていきます。さらに悪いことに優秀なエンジニアからやめていきます。なぜなら、エンジニアは売り手市場であり、優秀なエンジニアであればよい条件で次のポストを見つけることができるからです。
優秀なエンジニアは「代替が効きにくい人」でもあります。特に

  • ドメイン知識
  • 設計思想
  • 暗黙知

これらを持っている人がやめると、組織は大きな損失を被ります。

意思決定の負債の蓄積

技術負債という言葉がありますが、技術負債が発生する根本原因には意思決定の負債があります。ソフトウェアは設計思想がなくなると、途端に乱れます。設計思想は暗黙知として共有されていることが多く、設計思想の継承がされない状態でエンジニアがやめてしまうと次のような問題が顕在化します。

  • なぜその設計だったのか分からない
  • 誰も判断できず、決定が遅くなる
  • 場当たり的なコードが増え、開発速度や品質が低下する

意思決定の負債が発生(根本原因)→場当たり的なコードで対応(原因から引き起こされる結果)→場当たり的なコードが蓄積した結果、技術負債が発生(表面に見える現象)というのが技術負債の構造です。

沈黙が組織文化に与える長期的な影響

沈黙は個人の問題ではなく、組織文化の問題として広がっていきます。優秀なエンジニアが黙り始めると、その影響は組織全体に波及します。

残ったエンジニアは「意見を言っても無駄だ」という文化を学習し、自分も意見を言わなくなる傾向があります。このように、沈黙は組織文化として定着し、長期的に組織の意思決定力を弱めていきます

また、心理的安全性が低下することで、エンジニアはリスクを取らなくなり、イノベーションが生まれにくくなります。組織は現状維持に留まり、競争力が低下していくことになります

6.経営者・事業責任者・リーダーが取るべき視点

まず、意思決定のプロセスのまずさがエンジニアの退職を引き起こしていると認識することが第1歩です。
意思決定のプロセスについてはリーダーの影響が大きいため、現場のエンジニアレベルでの改善は難しいということを理解する必要があります。

意思決定のプロセスは一見すると人の問題に見えますが、「人の問題ではなく、構造の問題」です。本質的な問題は意思決定のプロセスが整備されていないところにあります。構造の問題は水が高いところから低いところに流れるように、再現性を持って繰り返されます。そのため、新しく人を入れても数年で離職ということを繰り返すことになります。

沈黙を生まない組織設計が必要

意思決定のプロセスの成否は仕組みとマネジメント層の適切な運用にかかっています。意見を言いやすい環境づくりをすることで、エンジニアが意見を言い続けられる環境を作る必要があります。

意見を言いやすい環境づくり

意見を言いやすい環境を作るためには、以下のような取り組みが有効です。

  • 意見を言うこと自体を評価する: 意見の内容が採用されなくても、意見を言ったこと自体を評価することで、意見を言いやすい環境を作ることができます
  • 意見の違いに寛容である: 意見の違いは組織の多様性であり、長期的な組織力の源泉です。意見の違いに寛容であることで、多様な意見が出やすくなります
  • 意見を言うリスクを下げる: 意見を言うことで不利益を被るリスクがあると、意見を言いにくくなります。意見を言うこと自体がリスクにならないような環境を作ることが重要です

定期的なフィードバックの仕組み

意思決定プロセスが適切に機能しているかを定期的に確認する仕組みを作ることも重要です。例えば、定期的に「意見を言いやすい環境かどうか」をアンケートで確認したり、1on1で意見を聞いたりすることで、問題を早期に発見できます。

沈黙を生まない組織設計は、一度作れば終わりではありません。継続的に改善していく必要があります。組織の状況に応じて、適切な仕組みを調整していくことが重要です。

おわりに

「沈黙は同意を意味しない」という言葉があります。同意のない沈黙は組織の危険シグナルです。組織はそこで働く人が建設的な意見が言えるように意思決定プロセスを作り、適切に運用する必要があります。そうでなければ、優秀な人材を失い、組織の競争力が低下することになるでしょう。

この記事が、あなたの組織でも起きているかもしれない問題に気づくきっかけになれば幸いです。

2025年の振り返り|フリーランス1年目に学んだ、判断・構造・距離感

はじめに

2025年という1年が、静かに終わりへと向かっています。今年はフリーランスとして本格的に活動を始めた年であり、技術的にも個人的にも大きな変化があった1年でした。今年1年の技術面での学び、成長を感じた点を振り返り、少し先の未来に向けた展望を整理したいと思います。

技術面での学び

Go / ReactによるマルチテナントSaaSの開発

2025年は開発に集中をした1年となりました。
ここ数年のキャリアはマネジメント中心であったり、開発をしつつチームビルディングを行うことが多かったのですが、マネジメント方面の仕事はほとんどやらず、開発や設計についてじっくり腰を据えて取り組むことができました。
技術スタックは、バックエンドがGo/Node.js、フロントエンドがReactです。
マルチテナントSaaSの開発に携わることができ、マルチテナントSaaSの設計について開発を通して理解することができました。

AIを使った開発

この1年で開発方法が大きく変わりました。従来の人間が全てを行う開発からAIを活用した開発がこの1年で大きく変わったポイントです。私の18年のキャリアの中で最も大きな変化だと感じています。
コーディングだけでなく、仕様の調査や設計、コードレビューなど多くの領域でAIを使うようになりました。
AIによる開発は今までの人による開発と比べて開発速度、品質ともに向上すると断言できます。
一方でAIは万能ではなく、最終的な品質を決めるのは人間の判断であることも同時に学びました。
こういったことを知識ではなく、現場で肌で感じれたことが貴重な経験となりました。

「判断」「構造」「距離感」の学び

技術的に収穫のあった1年となりましたが、特に成長として実感していることは技術そのものというより、仕事の進め方や思考の質が変わったことにあると考えています。特に「判断」「構造」「距離感」の3つのポイントについては言語化できるレベルで明確な学びがありました。

判断|状況に応じた判断と振る舞い

以前は「自分で判断しなければならない」「自分が決めるべきだ」という思いが強く、時としてまわりとの衝突に繋がってしまう恐れがありました。

あらためて、いちプレイヤーとして働くようになり、それに伴い仕事での観察や内省を通して、

  • 判断に必要な情報を整理して提示する
  • メリット・デメリットやリスクを言語化する
  • 最終的な判断は相手に委ねる

という進め方のほうがスムーズにいくのではないかと考えました。


結果として、

  • まわりとの摩擦が減る
  • エネルギーを無駄に消耗しない
  • 決定者が明確になり意思決定の速度が上がる

といった効果が生まれ、仕事がスムーズに進むようになったと感じています。
主体性がなくなったのではないかとの懸念を持たれるかもしれませんが、「判断を委ねること」に価値を置けるようになったということが本質的な変化であり、状況に応じて適切な判断や振る舞いができるようになったのではないかと思います。

構造|仕組みに目を向ける

問題を「構造の問題なのか、個人の問題なのか」で切り分けて考えられるようになったことが以前と比べた大きな違いです。
以前は、現場で起きる違和感やトラブルに対して「能力や姿勢の問題ではないか」「自分が何とかすべきではないか」と考え、解決の難しい領域にエネルギーを消耗してしまうことがありました。

現在は構造に問題がないかという観点で捉えるようになりました。
具体的には次のように考えます。

  • 役割設計や責任範囲に歪みはないか
  • 意思決定やレビューの仕組みに不備はないか
  • ルールや前提が適切に共有されているか

構造の問題に対しては、前提や仕組みを少し変えるだけで、再発防止や影響範囲の縮小といった効果的なアプローチが取れることを学びました。
一方で、個人の能力や性格、価値観に起因する問題については、それが自分の立場で解決できる領域なのかを判断し、自分が介入すべきでないものには介入しないと割り切れるようにもなりました。
このように問題の対象について識別することで、より効果的に時間とエネルギーを使えるようになったと思います。

距離感|適切な余白を保つ

仕事との間に、適切な余白を置けるようになったことも、この1年での変化です。
距離感が近いと相手や組織への期待であったり、その期待とのギャップが生じます。
期待とのギャップからはネガティブな感情が生じやすく、不要にエネルギーを消耗してしまい、安定したパフォーマンスを発揮するのが難しくなってしまいます。
安定したパフォーマンスを発揮するには、適度な距離感を持つほうがよいことがわかってきました。


また、コードレビューへの向き合い方にも変化がありました。 以前は、コードレビューで多くの指摘が入ると少なからず気持ちが揺れていましたが、こちらについても感情とは切り離して淡々と対応することができるようになりました。 この変化はAIによるコーディングができるようになり、対応コストが下がったことも影響していると考えています。

この距離感が身についたことで、不要にエネルギーを消耗せず、安定して価値を出し続けられる感覚を持てるようになりました。

以上のような変化は正社員→フリーランスへの立場の変化とAIによる開発手法の変化が影響していると思っており、継続的にポジティブな効果をもたらすと考えています。

これからについて

この1年間を通して、自分の強みは「実装力」そのもの以上に、構造の問題を識別しそれをどう解決するかを考えられることにあると、よりはっきりしてきました。

なぜそれができるのかというと、18年間の現場経験の中で、さまざまなチーム・プロダクト・フェーズを見てきたことで、何が機能しやすく、何が形骸化しやすく、どこで現場が詰まりやすいのかを肌感覚として理解できているからだと考えています。
理論として正しいかどうかだけでなく、「この現場で本当に回るか」「いま導入すべきかどうか」といった観点で判断できることは、これまでの経験が積み重なった結果です。

こういった自分の強みがわかってきたため、今後はより包括的な技術判断が求められる仕事に携わっていきたいと考えています。
現場で手を動かしつつも、プロダクトやチームが前に進むための「判断」と「構造」に関わる役割を担うことで、より大きな価値を提供していければと思います。

おわりに

この1年間を振り返ると表面的な飛躍的成長というよりかは内面的に成長したと思える1年でした。技術・働き方・メンタルの一つひとつの学びが深く、実りのある1年だったように思います。


最後に少し抽象的な話しになりますが、今まではどちらかというと燃え盛る炎に象徴されるようなエネルギーや情熱に魅力を感じていましたが、そういったものは一時的で長く続くものではありません。もちろん、時にはそういったエネルギーも必要ですが、今は雪の荒野に静かに立つ常盤木のように静かで持続的な生命力に魅力を感じます。 この常盤木のように静かに、歩みを止めず、持続的に成長していけたらと感じるこの頃です。

今回は1年の振り返りということで個人的な内容となってしまいましたが、同じようにソフトウェアエンジニアやフリーランスとしての働き方に興味がある方の参考になれば幸いです。

TypeScriptで多言語システムの型チェックを実装する

概要

多言語対応は一般的に翻訳関数に翻訳キーを渡すことで実装されます。その際に問題になるのが、キー名の間違い、キー名の変更漏れなどによる実装不備です。このようなエラーを人の目で全て確認するのは難しいため、システムによる検知が有効です。

この記事では、TypeScriptの型システムを活用した、型チェックの方法を説明します。これにより、正しい翻訳キーが割り当てられていることがコンパイル時にチェックされ、アプリケーションの品質向上に大きな効果が期待できます。

多言語システムの実装

1. ベースとなる型の定義

翻訳の型チェックは、日本語のメッセージファイル(ja.ts)をベースとして構築します。翻訳ファイルはJSONファイルで扱われることもありますが、型チェックという観点ではtsファイルで管理するのがおすすめです。

1
2
3
import { jaMessage } from './ja'

export type JaMessage = typeof jaMessage

TypeScriptの機能:typeof演算子

typeof演算子は、値から型を抽出するTypeScriptの機能です。typeof jaMessageにより、jaMessageオブジェクトの構造から型を自動的に生成します。これにより、jaMessageの構造が変更されると、型定義も自動的に更新されます。

ja.tsのサンプルコード

以下は、日本語メッセージファイル(ja.ts)のサンプルです。このファイルが型定義のベースとなります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const jaMessage = {
term: {
next: '次へ',
back: '戻る',
cancel: 'キャンセル',
submit: '送信',
},
pages: {
home: {
title: 'ホーム',
},
about: {
title: '紹介',
},
contact: {
title: 'お問い合わせ',
},
terms: {
title: '利用規約',
},
},
} as const

このファイルでは、as constを使用することで、値がリテラル型として推論されます。これにより、型の精度が向上し、より厳密な型チェックが可能になります。

as constとは

as constは、TypeScriptの「constアサーション」という機能です。オブジェクトや配列にas constを付けることで、以下の効果が得られます。

  1. リテラル型として推論される: 値が具体的なリテラル型(例:'マイページ')として推論されます
  2. 読み取り専用になる: オブジェクトのプロパティがreadonlyになります
  3. 型の精度が向上する: より具体的な型情報が保持されます

as constがない場合

as constがない場合、TypeScriptは値の型を一般的な型として推論します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// as const がない場合
export const jaMessage = {
term: {
next: '次へ',
back: '戻る',
},
}

// 推論される型
// {
// term: {
// next: string; // 一般的な string 型
// back: string; // 一般的な string 型
// }
// }

この場合、typeof jaMessageで取得できる型は、値がstring型として推論されます。

as constがある場合

as constがある場合、TypeScriptは値の型をリテラル型として推論します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// as const がある場合
export const jaMessage = {
term: {
next: '次へ',
back: '戻る',
},
} as const

// 推論される型
// {
// readonly term: {
// readonly next: '次へ'; // リテラル型
// readonly back: '戻る'; // リテラル型
// }
// }

この場合、typeof jaMessageで取得できる型は、値が具体的なリテラル型('次へ''戻る')として推論されます。

なぜas constが必要なのか

翻訳メッセージの型チェックにおいて、as constは以下の理由で重要です。

  1. 型の一貫性: typeof jaMessageで取得した型が、実際の値の構造を正確に反映します
  2. 型の精度: GenericTypeが適用される際、元の構造が正確に保持されます
  3. 読み取り専用の保証: 翻訳メッセージが誤って変更されることを防ぎます

実際の違い

as constの有無による型の違いを確認してみましょう。

1
2
3
4
5
6
7
8
9
// as const がない場合
const message1 = { term: { next: '次へ' } }
type Type1 = typeof message1
// Type1 = { term: { next: string } }

// as const がある場合
const message2 = { term: { next: '次へ' } } as const
type Type2 = typeof message2
// Type2 = { readonly term: { readonly next: '次へ' } }

as constがある場合、nextの型は'次へ'という具体的なリテラル型になります。これにより、型システムがより正確に動作し、翻訳キーの型チェックがより厳密になります。

2. 再帰的な型変換

翻訳メッセージはネストされたオブジェクト構造を持っています。この構造を型として表現するために、GenericTypeというユーティリティ型を使用しています。

1
2
3
4
5
type GenericType<T extends object> = {
[K in keyof T]: T[K] extends object ? GenericType<T[K]> : string
}

export type I18nMessage = GenericType<JaMessage>

TypeScriptの機能:条件型(Conditional Types)と再帰的な型

  • [K in keyof T]: マップ型(Mapped Types)により、オブジェクトの各キーを反復処理します

  • T[K] extends object ? GenericType<T[K]> : string: 条件型により、値がオブジェクトの場合は再帰的にGenericTypeを適用し、そうでない場合はstring型とします

これにより、以下のような構造が正しく型付けされます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
term: {
next: string // 文字列型
back: string
cancel: string
submit: string
},
pages: {
home: {
title: string // 3段階のネストも正しく型付けされる
},
about: {
title: string
},
contact: {
title: string
},
terms: {
title: string
}
}
}

GenericTypeは再帰的に動作するため、何段階のネストでも正しく型付けされます。

3. 各言語ファイルでの型チェック

各言語ファイルでは、I18nMessage型を明示的に指定することで、型チェックが行われます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { I18nMessage } from './types'

export const enMessage: I18nMessage = {
term: {
next: 'Next',
back: 'Back',
cancel: 'Cancel',
submit: 'Submit',
},
pages: {
home: {
title: 'Home',
},
about: {
title: 'About',
},
contact: {
title: 'Contact',
},
terms: {
title: 'Terms',
},
},
}

型チェックの効果

  • 必須キーが欠落している場合、コンパイルエラーが発生します

  • 存在しないキーを追加しようとすると、エラーが発生します

  • 値の型がstringでない場合、エラーが発生します

  • キーのタイポがある場合、エラーが発生します

4. 翻訳関数の型安全な実装

翻訳関数はライブラリを利用してもよいですが、シンプルな翻訳システムであれば自前で実装するのもよいと思います。ここでは、必要最低限の翻訳関数のサンプルを紹介します。

翻訳関数についても型安全を担保する必要があります。型システムを実装するために、Paths型とPathValue型を使用します。

キーパスの型生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type PathsInternal<T> = T extends string
? never
: T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? T[K] extends string
? K
: `${K}.${PathsInternal<T[K]>}`
: K
: never
}[keyof T]
: never

export type Paths = PathsInternal<JaMessage>

この型により、'term.next''pages.home.title'のような文字列リテラル型が自動生成されます。

キーパスから値を取得する型

1
2
3
4
5
6
7
8
9
10
11
export type PathValue<T, P extends string> = P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? T[Key] extends object
? PathValue<T[Key], Rest>
: never
: never
: P extends keyof T
? T[P] extends string
? T[P]
: never
: never

この型により、キーパスから対応する文字列型を取得できます。

翻訳関数の実装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { I18nMessage, Paths, PathValue } from './types'

export type I18nFunction = <P extends Paths>(
key: P
) => PathValue<I18nMessage, P>

export function createI18n(message: I18nMessage): I18nFunction {
return function <P extends Paths>(key: P & string): PathValue<I18nMessage, P> {
const keys = (key as string).split('.')
let value: any = message

for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k as keyof typeof value]
} else {
throw new Error(`Translation key "${key}" not found`)
}
}

if (typeof value !== 'string') {
throw new Error(`Translation key "${key}" does not point to a string value`)
}

return value as PathValue<I18nMessage, P>
}
}

使用例

翻訳関数の使用例です。

翻訳関数の使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createI18n } from './i18n'
import { jaMessage } from './ja'
import { enMessage } from './en'

// 日本語の翻訳関数を作成
const tJa = createI18n(jaMessage)

// 英語の翻訳関数を作成
const tEn = createI18n(enMessage)

// 使用例
console.log(tJa('term.next')) // '次へ'
console.log(tJa('pages.home.title')) // 'ホーム'
console.log(tJa('pages.about.title')) // '紹介'
console.log(tJa('pages.contact.title')) // 'お問い合わせ'
console.log(tJa('pages.terms.title')) // '利用規約'

console.log(tEn('term.next')) // 'Next'
console.log(tEn('pages.home.title')) // 'Home'
console.log(tEn('pages.about.title')) // 'About'
console.log(tEn('pages.contact.title')) // 'Contact'
console.log(tEn('pages.terms.title')) // 'Terms'

正しい使用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const enMessage: I18nMessage = {
term: {
next: 'Next', // ✅ 正しい
back: 'Back',
cancel: 'Cancel',
submit: 'Submit',
},
pages: {
home: {
title: 'Home', // ✅ 3段階のネストも正しい
},
about: {
title: 'About',
},
contact: {
title: 'Contact',
},
terms: {
title: 'Terms',
},
},
}

エラーが発生する例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export const enMessage: I18nMessage = {
term: {
next: 'Next',
// ❌ エラー: 'back' プロパティが必須だが欠落している
},
}

// ❌ エラー: 'unknownKey' は存在しないキー
export const enMessage: I18nMessage = {
term: {
next: 'Next',
unknownKey: 'Value', // エラー
},
}

// ❌ エラー: 翻訳関数で存在しないキーを指定
const t = createI18n(enMessage)
t('term.invalidKey') // コンパイルエラー
t('pages.home.invalid') // コンパイルエラー
t('invalid.path') // コンパイルエラー
t('pages.invalid.title') // コンパイルエラー

メリット

  1. コンパイル時の安全性: 翻訳キーのエラーを実行前に検出できます

  2. 自動補完: IDEが翻訳キーを自動補完してくれます

  3. リファクタリングの安全性: キー名を変更する際、使用箇所を自動的に検出できます

  4. ドキュメントとしての役割: 型定義自体が、利用可能な翻訳キーのドキュメントとなります

  5. 翻訳関数の型安全: 翻訳関数を使用する際も、存在しないキーを指定するとコンパイルエラーが発生します

サンプルコード

サンプルコードは https://github.com/shoyan/i18n-sample で公開しています。

12331