UPSIDER Tech Blog

支払い.com のアジャイル開発を基本としたスクラムではない開発手法について

はじめに

UPSIDER の支払い.comでバックエンドエンジニアを担当している水村です。

リリース当初はビジネスサイドとエンジニアを合わせて5、6名だった支払い.comのチームですが、リリースしてから2年弱でビジネスサイドを含めると数十名規模になりました。

今回は、アジャイル開発を基本としていますが、スクラム開発ではない支払い.comの開発手法についてまとめました。

支払い.comのリリースフロー

2022年5月に正式スタートした支払い. comですが、2023年11月22日現在、本番環境へのリリース回数は394回に達しました。過去1年半の営業日を約370日と考えると、ほぼ毎日リリースしていることになります。毎日リリースが行われるわけではありませんが、時には1日に複数回リリースすることもあります。開発チームの人数が多くないにも関わらず、この頻度でリリースができていることは良い成果だと思います。

さらに、これまでのリリースは特に大きなバグも少なく、安定して行われています。たまに私が間違えてリリースすることもありましたが、そうしたやらかしは2回程度で、幸い本番環境への影響はありませんでした。翌日にリリースしようと思っていた軽微な修正を早めにリリースしてしまった、という感じです。

支払い.comはスクラム開発を採用していないので、スプリントがありません。ですが、アジャイル開発やスクラム開発の考え方を基本としています。機能が出来上がってテスト環境でテストして問題なければリリースという流れで本番リリースを行なっています。本番リリースまでのリードタイムを短くした方がメリットがあると考えているため、リリース準備が整った機能はどんどんリリースしています。これは和田卓人さんの「質とスピード」や「Lean と DevOps の科学」などから影響を受けています。

リリース頻度が高いことのメリット

良い意味で本番リリースの緊張がなくなることが最大のメリットだと感じています。リリース頻度が2週間に1回だったり、月に1回だった頃と比べても安心してリリースできている実感があります。小さな変更や機能追加を頻繁にリリースできるのでテストする範囲も狭いです。テストもすぐ終わります。いいことづくめなので頻繁にリリースしたくてたまらなくなってきます。

リリース頻度を高めるためにやっていること

当たり前のことすぎるので書く必要なさそうですが、GitHub Actions 上の CI でテストを自動化して CD で簡単にデプロイできるようにしてます。CI は早く終わったほうが嬉しいので定期的に見直して遅くなったら改善します。

Pull Request を作ると CI が自動で走る。テストは並列化されている。

本番リリースも GitHub Actions 上で実行されています。バックエンドの場合は Docker Image を作成して Artifact Registry に push して Cloud Run へのデプロイが実行されます。フロントエンドの場合は GitHub Actions 上で Next.js のビルドを実行し Firebase Hosting にデプロイします。

リリースについての基本的な考え方ですが、最小限の機能や変更を細かくリリースするようにしています。「GitLabに学ぶ 世界最先端のリモート組織のつくりかた」に書いてある考え方と近いかもしれません。この本は最近読みましたが、参考になることが多かったです。

スプリントをやらない理由

スプリント方式を採用しない理由は、主にリードタイムが長くなることにあります。スクラム開発では、1-2週間のスプリントを実施し、その期間内にユーザー受け入れテスト(UAT)まで完了しリリースできる状態にすることが目標です。しかし、実際には1つのスプリント内で UAT を完了させるのは難しく、多くの場合次のスプリントに持ち越されます。これにより、機能が本番環境にリリースされるまでには最短で1〜2週間の遅延が発生します。

さらに、スプリント内でリリース対象の機能が多い場合、テストと本番リリースが困難になることがあります。テストで多くのバグが見つかった場合、機能を元に戻したり(revert)、特定の機能を選んでリリースしたり(cherry-pick)する必要が出てきます。また、スプリントの期限に合わせて品質を犠牲にしたコードがマージされることもあり、これは開発の負債となり得ます。

確かにスプリント方式が有効なケースもありますが、現在の私たちの開発体制ではその必要性が低いと感じています。スプリントによって生じる遅延や複雑さを考慮すると、私たちにとって最適な方法ではないと判断しました。

よって、支払い.comではカンバンのような開発方式を採用してます。エンジニアの手が空いたら優先度の高いタスクを順番にやっていくようにしています。

そもそもなぜスプリントが必要なのかを考えると、ステークホルダに対して進捗を伝えやすいことが大きな理由だったりします。UPSIDERのように自社でサービスを運営している会社の場合、進捗に対して説明責任が必要な外部のステークホルダがあまりいないです。

開発の細かい進捗は支払い.comのチーム内で共有できていればよく、リリースタイミングは自分たちでほぼ自由に決めることができます。そのため、スプリントを採用する必要性は、一般的な環境とは異なるかもしれません。スプリントプランニングとは別に、顧客に何を提供したいのかを機能単位で判断して優先順位づけを行なっています。

プロダクトバックログ

以前はプロダクトバックログGitHub Projects で管理していたのですが、エンジニア以外のメンバーが見づらかったりしたので notion に移行しました。notion に移行するにあたってLayerX さんのテックブログが参考になりました。ありがとうございます!

モザイクだらけですが、要件と仕様とタスクを notion で管理しています

プロダクトバックログ上で優先度の高いものから要件と仕様を詰めていきます。ビジネスサイドの希望するリリース日が実現可能かどうかの見積もりをエンジニアが行います。見積もり精度はあくまで目安であり、70%の確率で2-3日で完了、90%の確度で4-5日といったブレがあるので、見積もり段階で決定したリリース日からずれることが想定されます。

個人的には見積もり段階でいつ頃リリースできそうかを判断し、実装途中でさらに精度が上がってくると思うので、途中経過を報告していつ頃リリースできそうかをビジネスサイドと調整するのがいいのかなと考えています。また、見積もりはあくまで見積もりであり、コミットメントではないという前提で考えています。

リリース日をどうやって決定するのかは現在改善を行なっている最中です。ビジネスサイドの要望としてはなるべく早い段階でいつリリースできるのかを知りたいのですが、エンジニアとしては見積もりや実装途中までやってみないとなんとも言えない部分があるので、ビジネスサイドとエンジニアのお互いにとってどのあたりで合意を取るのが最適なのかを模索しています。

おわりに

支払い.comは元気な20代前半の若者が PjM, PdM, PMM を担当しています。エンジニアチームも20代中盤から40代、元 SIer や受託開発経験者など、多様性があります。最近の若者は非常に優秀で、めちゃくちゃ優秀ですごいな・・というのを感じながら若者の成長を見守りつつ、顧客にとって価値のある機能を最速でリリースできるように努めてまいりますので、よろしくお願いいたします。

UPSIDERはエンジニアを募集していますので、ご興味がありましたらカジュアル面談などのご応募をお待ちしております!

herp.careers

参考文献

アジャイル開発やスクラム開発についての考え方は以下の本などを参考にしています。

アートオブアジャイルデベロップメント

エッセンシャルスクラム

エクストリームプログラミング

アジャイルな見積もりと計画作り

アジャイルサムライ

アジャイルレトロスペクティブズ 強いチームを育てる「ふりかえり」の手引き

(冊子版)ふりかえり読本 実践編~型からはじめるふりかえりの守破離~

Lean と DevOps の科学

アートオブプロジェクトマネジメント

UPSIDERのフロントエンドをNuxtからNextへリプレイスしている話

はじめに

こんにちは、株式会社UPSIDERでフロントエンドチームに所属している久保です。

今回は、リリースから走り続けてきた法人カード「UPSIDER」(以下、UPSIDER)のフロントエンドを、NuxtからNextにリプレイスしている話を共有します。

リプレイスに至った背景

背景はいくつかあるのですが、主にNuxt3へのリプレイスコストが大きかったことが背景としてあげられます。UPSIDERはNuxtの2系で実装されており、TypeScriptを使用するためにvue-property-decoratorを使用しています。Nuxt3では元々破壊的変更が大きい中で、classベースのコードからNuxt3へのリプレイスコストは払えないという判断をしました。仮に、Nuxt3にリプレイスできても3系に対応していないライブラリの再選定も必要になり単純なリプレイスでは収まらない可能性がありました。

Next.jsを採用した理由

まず、リプレイスコストを最小に抑えつつ将来的に実現したいことが可能かどうかを判断軸としました。

リプレイスコストの観点では、vueの構文からjsxになるため1からのコーディングにはなりますが、vueをサポートしているライブラリはreactもサポートしてるものも多く、ライブラリをそのまま流用できるといったことからリプレイスコストを下げられるといった判断をしました。

また、将来的にLPはSSGを使用し静的なものとして配信したいといった構想があるためページ毎にSSGを実現できることは必須でした。

これらの判断を加味し、総合的に見てもエコシステムの充実度や実用例の数からデファクトスタンダードになりつつあるNext.jsを採用しました。

リプレイス後の技術

  • Next.js@13系
  • MUI@5系
  • Storybook@6系
  • react-query@3系
  • zod@3系
  • React Hook Form@7系

また、スキーマ駆動での開発を行なっているため、OpenAPIを用いたスキーマ定義から型定義とapi clientの自動生成も行なっています。

リプレイス戦略

リプレイス方法

UPSIDERは全体で約120のページが存在しています。そのため、全てをリプレイスしてからリリースするという、ビックバンリリースは実装やQA観点からもコストが大きいという判断をし採用を見送りました。他にも、マイクロフロントエンド的にリプレイスしていく案もありましたが、基盤が複雑になることを筆頭に現段階では諸々の恩恵を受けづらいのでは?となりこちらも採用を見送りました。

最終的には、ページ毎にリプレイスをしていくという一般的な手法を採用しました。routingに関しては、本番はIstioを用いてAというpathはNextへ、BというpathはNuxtにroutingさせています。開発環境に関しては、node.jsで簡単なプロキシを立てpathに応じてNextとNuxtにroutingさせています。

Screaming Architectureの導入

リプレイスするにあたりディレクトリ構成も見直しました。Nuxtのディレクトリ構成はファイルタイプで管理していく構成をとっており、ページ数やコンポーネントが多くなるにつれて見通しが悪くなっていました。また、Nuxtではほとんどが規約化されていないことも拍車をかけ、実装者に依存する形になっておりファイルタイプをどこで管理するのかという点においても一貫性がない状態でした。

これらを解決するためにScreaming Architectureを採用しました。(Features directoryとも呼ばれている認識です)

dev.to

簡単に説明すると、対象のFeatureに関連するファイルはFeature内に閉じ込めることを意識してディレクトリを構成しファイルを配置していきます。そのため、関連するファイル群が散らばりにくくなっており、対象ディレクトリの中を見れば、ほとんどはそのディレクトリの中にあるという状態を実現できます。

src/
└── features/
    ├── some-feature-A/
    │   ├── component-A/
    │   │   ├── index.ts
    │   │   ├── use-some-hook.ts
    │   │   ├── type.ts
    │   │   ├── styles.module.scss
    │   │   └── component-A.tsx
    │   ├── component-B
    │   └── component-C
    └── some-feature-B/
        ├── component-A
        ├── component-B
        └── component-C

もちろん、Feature間で共有したいロジックやコンポーネントは存在するため、src/componentsやsrc/utilsなどのグローバルのディレクトリは存在しているのですが、基本的にはFeatureの中で閉じるルールになっているため今の所ディレクトリの肥大化は抑えられています。

スキーマ駆動開発の導入

今までは、APIのドキュメントがなく実装者が直接responseやコードを見て型定義をしていました。新しいAPIに関しては、Notionにスキーマを書いて共有するという形をとれていたのですが、開発者が型定義しないといけないことは変わりはありませんし、時間が経つとスキーマを書いているNotionも見つけづらい状態になっていました。

上記の問題を解決するため、OpenAPIによるスキーマ定義を導入し、定義したスキーマから型とapi clientを自動生成もできるような仕組みを整え、実装者が開発に集中できるような環境を作りました。

また、スキーマのexamplesをjsonに変換する仕組みも整えることにより、フロントエンドがバックエンドの開発に依存しなくなった点も大きなメリットを感じています。

Storybookを使ったInteraction testの導入

今までは、実装したものは全て実装者およびQAで確認作業を行うというフローになっていました。しかし、これは資産にならずプロダクトが肥大化するにつれ確認作業も膨大な量になっていき、確認コストがかかり続けリソースを消費し続けてしまいます。

上記の問題を解決するためNext側ではStorybookを使ったInteraction testを導入しました。

storybook.js.org

導入した結果、当初課題としていたQAリソースの圧迫は軽減に向かっています。また、リファクタにおいても効果を発揮しており、テストが存在することによって安心してリファクタができる環境が整ってきています。

まとめ

「UPSIDERのフロントエンドをNuxtからNextへリプレイスしている話」を共有しました。

Nuxtが支えてきた基盤に敬意を払いつつ、より良いプロダクトを実現するためにNextへリプレイスしています。

今回は概要などの軽い紹介でしたが、今後はより深い部分も紹介できればと思っています。

宣伝

UPSIDERでは、成長企業のための法人カード「UPSIDER」と、すべてのBtoB取引でクレジットカードを利用できる請求書カード払いサービス「支払い.com」を提供しており、どの方面においてもエンジニアの採用を行なっております。

カジュアル面談もやっておりますので、少しでもご興味のある方は、ぜひご連絡ください!

herp.careers

4年続けてきた本番運用を(ほぼ)完全にバトンタッチした話

みなさんこんにちは、UPSIDERでエンジニアをしています清水(通称シミケン)です。

僕はUPSIDERの創業期から関わらせてもらっており、Webシステムの設計・開発をメインでやってきたこともあって最初にプロダクトをローンチした2019年12月以来、ずっと本番運用にも関わってきました。

もちろん、フェーズによって関わり方は徐々に変わってきていたのですが、この度ようやく本番運用に関する業務を(ほぼ)すべて引き継ぎ、他のメンバーに運用をお任せできるようになったため、これまでのプロダクトやチームの変遷も踏まえて自分がどんなことをしてきたのかを振り返ってみたいと思います。

1年目(プロダクトローンチ最初期)

今でこそ非常に多くのお客様から法人カードをご利用いただいていますが、プロダクトローンチ当初、法人カードはまだ提供できておらず、振込処理の自動化機能の提供を行っていました。

(そのあたりの流れは以前執筆させていただいたこちらの記事に詳しく記載しています)

ユーザー様がアップロードされた請求書をデータ化して振込まで一気通貫で行うサービス(現在このサービスは終了しており、別プロダクトとしてローンチした支払い.comに引き継がれています)だったのですが、最初は共同代表の宮城や水野を含む社内メンバー総出で手動でデータの入力を行っていました。

宮城もデータ入力してました

月次バッチの運用を自動化しようとして間に合わなかった模様

こんな感じで、ユーザー様へ新しい価値を届ける機能開発に追われつつちょっとずつ運用効率を改善していったり、まだ存在していない運用上必要な機能をなんとかギリギリで実装したり、色々と泥臭く運用を繋いでいた記憶があります。

ちなみに、当時Webシステムのサーバーサイドの社員は僕一人だったこともあり、システム側の運用はほぼワンオペで回していました。

2年目(法人カードローンチ期)

ついに念願の法人カードをローンチでき、運用タスクもカードに関するものが徐々に増えていきました。

この頃には請求書のデータ化も外部に委託する体制が整っていたため、ようやく社内総出でデータ入力する作業からは解放されたのですが、それでもデータの最終チェック・登録作業は社内で実施していたためまだそのあたりのタスクが残っており、結局ユーザー数の急増&カードに関する運用タスクの増加と相まってあまり業務量としては減っていなかった(というか増えた?)ように感じます。

毎年恒例、あけおめバッチ処理もやってました。

年始に限らず、毎月初に実施するバッチ処理は僕らのサービスの価値を支える一つの要素になっており、

  • 前月の利用実績に基づいて付与されるポイントを、確定できるようになった月初のタイミングで即座に付与し、そのまま翌月の請求金額から差し引けるようにする
  • 毎月の請求額をできるだけ早くユーザーにお伝えする
  • 月が変わったことで初めて連携が可能になる情報を即座に会計システムに連携することで、月次決算の早期締めに貢献する

などの価値を実現するため、月が変わったその夜のうちに処理を実行して、朝までには必要な処理を終えておく必要があります。

完全に自動化できればよかったんですが、上記のとおり朝には処理が終了している必要があるため、不測の事態に備える意味もあって結局今にいたるまで月初処理は必ず人が張り付いて対応しています。

ちなみに、当時Webシステムのサーバーサイドの社員は僕一人だったこともあり、運用はほぼワンオペで回していました。(2年連続2回目)

3年目(運用チーム組成期)

プロダクトローンチから3年目に入って、今まで提供してきた振込処理の自動化機能を終了することが決まり、ここに割かれていた運用リソースがごっそり不要になりました。

そして時を同じくして、ついについに念願のサーバーサイドの正社員メンバーが増え、運用チームが組成されました。

これらによって本格的に本番運用をチームで回していく体制が整っていき、僕自身が運用に割くコストは大幅に減りました。ようやく運用時にバイネームでメンションされまくる時代にさよならすることができたのです。本当に一緒に運用をやっていくことを快諾してくれたメンバーには頭が上がりません。

一方で、法人カードのリリースから一年が過ぎ、本格的に認知度があがってきてユーザー数が一気に増えるようになったのもこの時期です。システムの負荷が上がることによる問題が出始めるだけでなく、それまでは想定していなかったイレギュラーな使い方をされることも増えてきたため、そうしたイレギュラーな利用方法にも対応できるような修正が必要になってきました。

ただ、ここでも運用チームを組成した恩恵があり、徐々に運用改善にコストもかけられるようになりました。上記のようなイレギュラーケースへの対応ができたり、これまではTech側に依頼をしてもらう必要のあったことがビジネスサイドだけで完結できるようになったりと、そういった意味でもチーム化の意味は非常に大きかったです。

ちなみに、毎月恒例の月初バッチだけはしばらく自分が担当していたんですが、2022年10月くらいからそれもお任せするようになりました。(年始のあけおめバッチだけは、なんとなくお願いするのが憚られて自分でやりましたが)

4年目(バトンタッチ期)

そして、ようやくほぼすべての運用をバトンタッチすることができたのはつい最近のことです。

とはいっても、もう今年に入ってからはほとんどの運用タスクをチームにお任せしていたため、残っていたのはほぼ今年リリースした仕訳機能に関するお問い合わせ対応やバグFIXくらいでした。

それもようやく先日必要な引き継ぎを終え、ごく稀に質問をもらう程度でそれ以外の運用タスクはすべてチームが巻き取ってくれています。

おそらく2024年のあけおめバッチも、ついに初めてチームメンバーにお願いする形になるのではないでしょうか。(とかいいつつ何故かまたやっているかもしれませんが)

これまでを振り返って

プロダクトローンチ当初からずっと本番運用をして酸いも甘いも味わってきた身としては、それを他の仲間に任せられるようになって、肩の荷が下りた気持ちが3割、頼もしい仲間に支えてもらえてありがたい気持ちが6割、ちょっとだけ寂しい気持ちが1割といった感じです。

自分にとってこのプロダクトは本当に子供のような存在で、誕生当初は「この対応を〜日までに終わらせないとこのプロダクトは死ぬ」みたいなこともあり、なんとかそれを乗り切るために死にものぐるいでしたが、改めて振り返ると本当に楽しく素晴らしい経験をさせてもらっているなと感じます。

今ではUPSIDERは本当に多くのユーザー様にご利用いただけるプロダクトになりました。属人的な頑張りでなんとか支えていくフェーズはとうに終わりを告げ、これからはさらに安全・安心にご利用いただけるようにより強固な仕組み・組織を構築していく必要があります。

そうした運用面も含め、本当に実現したい価値からはまだまだほど遠い状態ですので、爆速でそうした価値を提供していけるように頑張っていきたいと思います!

Tech Blog Reboot!

はじめまして、UPSIDERで2023年8月からVPoEを務めている泉です。

前回の投稿からほぼ一年、間が空いてしまったようですが、やはり発信していくことでエンジニアコミュニティーとのエンゲージメントを深めたり、会社のカルチャーや、技術・プロダクトを紹介する良い場になると考え、再稼働(Reboot!)していくというところで再稼働一発目の投稿を仰せつかりました。

今回は、「いま自分がフォーカスしていること」をいくつか紹介したいと思います。

UPSIDERは、これまでかなり爆速でサービスを展開し、瞬く間に数万社のユーザーに選ばれる法人カードのサービスを成長させてきました。

また「挑戦者を支える世界的な金融プラットフォームを創る」というミッションの基、請求書の後払いを可能にする支払い.com2023年8月に発表したUPSIDER Capital 等、昨年467億という調達で話題になりましたが、その資本をスピーディーかつシームレスに流通させるかという軸をぶらさずに、まさに「金融」という社会インフラをアップデートしていくことにコミットしています。

これだけの成長の裏には、解決しないといけない様々な課題が当然あり、TECH領域においても例外ではありません。

2023年9月に経営合宿を行いましたが、その中で最も重要視されたTECHの最優先課題は以下の通り:

  • 開発基盤改善
  • データ基盤整備
  • 共通基盤整備
  • TECH人材採用

ちなみに、経営合宿でこれだけディープにTECHのテーマをしっかり議論できたことには自分にとって非常に新鮮で、事業計画・事業戦略、競合環境、資本政策、プロダクト戦略等のテーマももちろん議論しますが、その一方でその実現に向けたイネーブラーがTECHにあること、あるいはその裏返しでTECHの課題が、事業進捗を遅らせることに直結することを認識し、経営チームでディープに向き合ってくれることが非常に心強かったです。

開発基盤改善

法人カード「UPSIDER」のサービス・アーキテクチャーは基本、モノレポ・マイクロサービス化されており、ちょっと古い言葉ですが、ユーザーが実際触れるUIを提供するSoE (System of Engagement)、決済取引や各種台帳を正確に記録するためのSoR (System of Records)、そしてそれをつなぐBFF、という形で整理をしつつあります。

現在開発のスピードにキャップをかけている1つの要因がこのBFFにあり、と言っても色々な過去の様々な変遷が絡みあって(リスペクトすべき過去の変遷!)BFF自体がかなり多くのビジネスロジックを持ってしまっております。

またTech Stack的にも SoE = JS(React/Vue)、BFF = Kotlin、SoR = Go、と3言語操らないとフィーチャーデリバリーができない、という状態にあります。Kotlinのエンジニアが不足しているのもあり、SoE, SoRの実装が終わっていてもBFFの開発がキャップになってしまい、思うように開発速度が上がらない。

その課題を解決するために現在 Rearchitecture のプロジェクトを進めており、BFFを本来の姿に差せること、あと技術スタックもFrontに合わせてTSにすることで、技術スタックのギャップを埋めてデリバリーを加速させることに取り組んでおります。

その他、Vue→Reactの移行プロジェクトや、テスタビリティー向上させるための「Flex Staging」プロジェクトなど同時並行で動かし、年内にもこれまでのデリバリー速度に一定制限をかけていた構造的な課題が解決できそうかと思っております。(これらについてもどこかのタイミングでより詳細に紹介できればと思います!)

データ基盤整備

次に指摘されたのがデータ基盤。

ビジネスメンバーがQueryを書いてバンバン分析したいのに、なかなかそれが実現できない!

これまでアプリケーションデータの一部をBigQueryにインポートして、RedashなどのBIツールで可視化するということをやってきたのですが、ほとんどがアプリケーションの生データに対して直クエリを行っており、だいぶ「秘伝のタレ」化してしまっています。

ビジネスが捉えたいドメインデータとアプリケーションデータはなかなか噛み合わないもので、いわゆるデータディクショナリの整理からファクトテーブルの抽出・ディメンションの定義など、かなり0ベースで構築を進めております。

データは、即時性の高い与信審査、毎日数万件の決済データを処理してリスク管理を実現するための生命線でもあり、ひいてはユーザーへの価値提供の源泉なのでここも強化にも取り組んでいます。

共通基盤開発

ここは少し時間軸は長めではありますが、これまでUPSIDERカード、支払い.comの認証基盤がバラバラだったこと、あるいは共通基盤が無いために、UPSIDERカード内でもサービスの循環依存などが起こっていたのですが、それを集約・収束していくための共通基盤開発も行っております。

TECH人材採用

そして、これらの技術関連の課題を解決するだけでなく、主戦であるプロダクト開発も当然行わなくてはなりません。ユーザーがプロダクトの機能不足でチャーンしてしまうことも事業的には痛手ですし、ユーザーがプロダクト価値を感じてくれたからこそここまで成長してきているのでそのモメンタムを加速して行かなければなりません。

アプリケーション開発においては、フロントエンジニアもまだ正社員では2名しかおらず、Goエンジニア、Kotlinエンジニアも足りておりません。

上に挙げた領域ではプラットフォームエンジニア、データエンジニアの助けも必要ですし、UPSIDER Coworkerを初めとする新たな領域の開発、その他MLエンジニア、そしてプロダクト構想を描くプロダクトマネージャー、プロダクトデザイナーもまだまだ実現したい世界を作るためには手が足りておりません。

そのため今はHRマネージャー自ら手綱を持ってエンジニア採用を強化する体制を敷き、採用チャネルの拡大、ブランディング強化を率いてくれております。当のエンジニアメンバーも採用にはかなり熱心に向き合ってくれ、まさに組織一体となって採用にもフォーカスを当てております。

かくいう自分もかなり色々な帽子をかぶって役割を担っていますが、すこーーーしずつ状況が改善してきているのを実感しております。

最後に

ということで、熱意有り余って色々書いてしまいましたが(それでもかなり端折ったつもりなのですが、汗)こんなUPSIDERをより深く広く知ってもらうためのTech Blog、また新たに発信していきたいと思っておりますので、末永くお付き合いいただけると幸いです!

spoti.fi

UPSIDER Tech.fmも開始したので乞うご期待!

Greetings!

Hello esteemed readers and fellow tech enthusiasts! I'm Izumi, the newly minted VPoE (Vice President of Engineering, if we're being formal) at UPSIDER. I took up the mantle in August 2023.

Has it really been a year since our last update? Time sure flies when you're having... well, meetings, coding marathons, and more meetings. Jokes aside, I firmly believe that by sharing our journey, we build stronger connections with the engineering community, offering a sneak peek into our company's vibrant culture, innovative technologies, and groundbreaking products.

So, when they handed me the proverbial mic and said, "Reboot the blog!", how could I resist?

Today, I'd like to share a glimpse of what's been keeping me and the team busy.

UPSIDER: Scaling Heights and Breaking Barriers

UPSIDER's growth trajectory has been nothing short of meteoric. Our corporate card service has earned the trust and adoption of tens of thousands of users. Our mission statement — "Creating a global financial platform that supports challengers" — isn't just a catchy tagline. Last year, we secured funding of a whopping 46.7 billion yen, which was instrumental in accelerating initiatives like shi-harai.com (an innovative post-payment of invoices solution) and the unveiling of UPSIDER Capital in August 2023.

But let's not get sidetracked - at the core, our focus remains on revolutionizing the very fabric of financial infrastructure.

Insights from the Management Offsite, September 2023:

At our recent management offsite in September 2023, we distilled the paramount TECH priorities of the coming months. Here's what topped the list:

  • Refining our development infrastructure
  • Laying down robust data infrastructure foundations
  • Building out a Unified Application Infrastructure
  • The Hunt for TECH Talent

On a light note, it felt like a cool splash of water to engage in deep-dive TECH conversations amidst all the management talk. While we do love our business plans, strategies, and competitive analyses, it's evident that TECH is the backbone supporting these visions. I’m heartened by how hands-on and engaged our management team was in addressing these crucial topics. It's clear we're not just all talk and no tech.

Refining Development Infrastructure

At the heart of our corporate card service, "UPSIDER", lies a blend of Monolithic Repository and microservices architecture. Though it may harken back to yesteryears, we've organized it into three primary systems:

  • System of Engagement (SoE): This is the user interface layer where users get their hands dirty.
  • System of Records (SoR): Our reliable scribe that meticulously logs payment transactions and various other ledgers.
  • BFF (Best Friend Forever... just kidding, it's Backend For Frontend): Our trusted bridge between the two systems.

A critical speed bump in our development highway is the BFF. Despite its storied past (we tip our hats to the previous transitions!), the BFF is laden with business logic.

Diving into our tech stack, we juggle three languages to roll out features: SoE gets its shine from Typescript (React/Vue), the BFF is crafted in Kotlin, and SoR rolls in Go. But there's a wrench in the works – a dearth of Kotlin maestros. This means that even if SoE and SoR are raring to go, BFF development hits a roadblock, putting a damper on our development pace.

Our current solution? The Rearchitecture project. We're on a mission to get the BFF back to its heyday and bridge the tech stack divide by transitioning to TS, which is more in line with the front end. We're optimistic this will put the pedal to the metal in terms of delivery.

And that's not all! We're juggling a couple of other projects – like our Vue-to-React switcheroo and the intriguing "Flex Staging" initiative aimed at amplifying testability. We're confident that by year-end, these infrastructural tweaks will rev up our delivery speeds. Stay tuned for more detailed updates on these projects - we're eager to spill the beans!

Elevating Our Data Infrastructure

Shifting our gaze, enhancing our data infrastructure stands out as a primary objective.

The burgeoning demand to craft queries and dissect data has left us in a bit of a pickle – it's proving to be quite the challenge.

We've dabbled in funneling some application data into BigQuery and presenting it using BI platforms like Redash. However, much of our querying happens directly on the raw application data, making it our closely-guarded "secret ingredient".

A mismatch lurks between the domain data the business yearns to seize and the existing application data. To rectify this, we're donning our construction hats, meticulously organizing a data dictionary, sifting out fact tables, and carving out dimensions.

It's no hyperbole to say that data drives our operations. From zippy credit screenings to managing the mammoth task of processing myriads of payment data daily for risk oversight, data is at the helm. Recognizing its vital role in delivering value to our users, we're pouring energy into fortifying this domain.

Advancing Shared Infrastructure

While this endeavor is a marathon rather than a sprint, we're knee-deep in curating a unified infrastructure. Our goal? Seamlessly merge the authentication infrastructure of UPSIDER Card and shi-harai.com. This masterstroke will untangle any service overlaps inherent to the UPSIDER Card.

Unified Application Infrastructure

While the journey is long-haul, we're actively constructing a cohesive infrastructure that merges the authentication platforms of the UPSIDER Card and shi-harai.com. This strategic move aims to streamline any service intricacies associated with the UPSIDER Card.

On the Hunt for TECH Talent

Beyond overcoming tech hurdles, our primary frontline remains product development. There's a real cost when users bow out due to lack of product features. Our current progress is driven by the undeniable value users see in our offerings, and it's imperative to maintain, if not amplify, this momentum.

Speaking of app development, our current roster includes a mere duo of full-time front-end developers, and we're somewhat starved of Go and Kotlin wizards.

But that's not all. We're on the lookout for platform savants, data engineering maestros, ML engineers, visionary product managers, and creative designers. With ventures like UPSIDER Coworker on the horizon, it's evident we need more hands on deck to shape the future we dream of.

Thankfully, our HR lead is seizing the initiative, amplifying our engineer recruitment drive. The entire engineering brigade is buzzing with enthusiasm about onboarding fresh talent, making talent acquisition a top organizational priority.

I, for one, juggle myriad roles, and it's heartening to sense a palpable uptick in our circumstances.

In Conclusion

My fingers may have run away with me in my fervor (despite efforts to be succinct!).

To offer a deeper dive into the world of UPSIDER, we're toying with the idea of a Tech Blog to foster ongoing dialogue. We're in this for the long haul, so we hope you'll join us for the ride!

Also, the launch of UPSIDER Tech.fm is on the horizon. Keep those ears perked up and stay tuned!

KotlinコルーチンでDBアクセスするときのアンチパターン

こんちには!

UPSIDERのWebチームでサーバサイドKotlinを書いているエンジニアのおかだです。

突然ですが、Kotlinコルーチン使ってますか? もちろん使ってますよね。

KotlinでWebアプリケーションを書くのであれば、リクエストごとにコルーチンを起動してハンドリングするでしょうし、その処理の中でDBにアクセスすることも多いだろうと思います。ですが、このありがちな処理を、ごく普通に書くだけで、わかりにくいバグを生む場合がある、そんな話を紹介します。

やってはならないことは単純。


アンチパターン
DBトランザクション内でsuspendする処理を、大量(DBコネクション総数を超える数)のコルーチンで並行実行してはならない


やってしまうとしたらバッチ処理ですかね。これをやってしまうと、コルーチンはコネクションを使い果たし、マイルドデッドロックとでも言うべき状態に陥ります。特に気をつけてほしいのは、これはスレッド数とはほぼ無関係だということです。スレッド数がコネクション数よりも少ないから大丈夫、とはならないのです*1

どのような機制でこの問題が起きるのか。図で見ていきましょう。

DBコネクションが8つ、スレッドが3つ、コルーチンが20個、という状況を考えてみます。車がスレッド、円柱の下に生えてる触手みたいなやつがコネクションです。スレッドが車のようなものだとは全然思ってないですけどね。コルーチンとの関係性で言えば、コルーチンが乗ってる感あるかなと・・・。図にするのって難しい。

まず、スレッドの数と同じ3つのコルーチンが動き出します。トランザクションを開始してコネクションをプールから借りてきます。

さてここで、アンチパターンに記載したとおり、この処理はトランザクションの途中でサスペンドします。たとえば、DBに何か保存したあとにサスペンド関数でメール送信する、とか。

ここで重要なのは、トランザクションを維持するためには、コネクションはそれぞれのコルーチンが持ち続けなければならないということです。UPSIDERではORMとしてExposedを使っていますが、Exposedはこのように振る舞います。仮にExposedでなくとも、ロジカルに考えて、こうする以外の選択肢ってあまり思いつきません。

最初の3つのコルーチンがサスペンドしたことで、スレッドが空きました。というわけで、実行されるのをワクワクと待ち構えている次のコルーチンがスレッドを割り当てられて、処理を始めます。またコネクションが3つ消費されます。

そしてこのコルーチンたちもトランザクション途中でサスペンドします。

問題は次です。コルーチンがサスペンドして行った処理は(この3台の車とは別のスレッド上で行ったことにしておきますが)、それほど時間のかかる処理ではなく、すでに完了しているとします。つまり、左上で -_- みたいな顔をしている連中の一部は、スレッドが空くのを待っているのです。

ですが、彼らはスレッドを割り当ててもらえません。最初から待ち続けている左下のコルーチンが優先されます。このコルーチンのスケジューリングアルゴリズムCoroutineDispatcher に依存するので、変更できる可能性がないわけではないですが、これをうまくコントロールする CoroutineDispatcher の実装を試みるのはおそらく無理筋です。

最終的には、コネクションはすべて、サスペンド中のコルーチンに持っていかれ、スレッド上にはコネクションを待つコルーチンが居座り続けることになります。

空くはずのないコネクションをどのくらいの間待ち続けるのかはコネクションプールの設定によりますが、仮に30秒だとすると、まだコネクションを得ていない12個のコルーチンがすべてコネクション取得を諦めてタイムアウトするまでに 30秒 * (12コルーチン / 3スレッド) = 120秒 かかります。そのあとようやく左上のコルーチンがスレッドを割り当てられ処理を再開、コネクションを解放していきます。

解決策

アンチパターンを構成する、トランザクションサスペンドか、大量のコルーチン起動、どちらかを回避しましょう。

1. トランザクション内でサスペンドするのをやめる

Kotlinコルーチンのアドバンテージをすこし犠牲にしてしまうかもしれませんが。条件次第では大した代償なく対応できるかもしれません。チーム開発で、この内部処理ではサスペンド禁止、というのを周知し守り続けるのはなかなか難しいものがありそう。

2. 一度に起動するコルーチン数を制限する

先にスレッド待ちしているコルーチンがいなければ、サスペンドしているコルーチンがすぐにスレッドを獲得して処理再開し、コネクションを解放できます。コルーチン数を制限する方法については「Kotlinでコルーチンの並行処理数を制限する」という記事で触れていますので、そちらも合わせて読んでいただければと思います。


career.up-sider.com

herp.careers

*1:スレッド数が逆にコルーチン数を上回るほど多い場合はこの問題は起きないのですが、そういうシチュエーションは普通なさそう

アカウント開設フォームのUI設計における大失敗と爆速改善ストーリー

こんにちは。Webチームでエンジニアとして働いている久保です。

今回は、アカウント開設フォームのUI設計における失敗と爆速改善ストーリーについて紹介していきます。


アカウント開設フォームの通過率が劇的に下がってしまった

UPSIDERをご利用頂く前に、独自の審査を行うために、企業ごとにアカウントを開設していただく過程があります。しかし、とあるリリース以降、アカウント開設のプロセスを通過する割合が極端に減少してしまった事がありました。これは弊社にとって、新規のユーザーが増えず、サービス全体の信頼感も落としかねない大きな問題となりました。

eKYCを完全に終えられていない・・・?

原因を調べたところ、どうも「eKYCを最後まで終えられていない」ということがカスタマーサポートからのヒアリングで分かりました。

具体的な数字を出すと、eKYCの審査通過率が30%ほどと正常な審査通過率(70%)の半分以下になってしまっており、そのせいでカスタマーサポートのスタッフのにも大量に問い合わせが来てしまい、リソースを逼迫する状態にもなっていました。

その原因の一つとして、eKYCフォームに外部ヘルプページに飛ぶリンクを複数設置していて、それらを閲覧しないと理解ができない入力フォームになっていたことが挙げられます。

「eKYC」は法律の概念が関わることから、現在のユーザーの状態を分かりやすく表示したり、これから何をする必要があるのかを適切に表示することが難しい部分です。そのため、さまざまな箇所でヘルプページを利用して解決しようとしていたことが原因の根底にあると、フロントエンドチームの分析により分かりました。

ヘルプページへ遷移せずともアカウント開設できるUIを目指す!!

フロントエンドチームでは、これらの問題を解決するために「ヘルプページへ遷移せずともアカウント開設できるUI」を最低の条件に置くとともに、改めて下記を共通認識とし、UIを刷新しました。

  • 適切なタイミングで適切な情報を提示する
  • 必要のない情報は出さないようにする
  • 直感的に理解できるUIを目指す

また、アカウント開設の入力フォームから開設が完了するまでのフローをFigmaに張り出し、他にボトルネックになってる部分がないかも調査しました。

下記画像は、改善に取り組んでいた際のFigmaです。このページ以外にも4,5ページFigmaが存在し、ボトルネックの洗い出しにプロジェクトメンバー全体で取り組んでいました。

正常な審査通過率に戻り、ボトルネックを解消!

結果的にUI刷新直後から、eKYC導入後の正常な審査通過率と言われる70%前後に戻り、カスタマーサポートへのお問合せも激減しました。

この経験があってからUIを設計する際に「本当にヘルプページがないとダメなのか?」と言う意識がチーム内で生まれ、あるべきUIを目指す姿勢ができたように思います。

まとめ

「アカウント開設フォームのUI設計における大失敗と爆速改善ストーリー」を紹介させていただきました。

最後までお読みいただきありがとうございました。

限られた時間の中で、どのようなUIを提供すべきなのかの判断は難しいところはありますが、それでもあるべきUIを考え、全力で開発・提供するというのはUPSIDERならではなのかなと思っています。

実は一方でリソース的な要因として、当時フロントエンドエンジニア、UI/UXデザイナー、PdMが不足していたことがあります。次の宣伝に続きますが、UPSIDERでは全方位採用を行っております。ぜひUPSIDERにお力をお貸しください。

宣伝

UPSIDERでは、挑戦者を応援する法人カード「UPSIDER」と、すべてのBtoB取引でクレジットカードを利用できるビジネスあと払いサービス「支払い.com」を提供しております。

UI/UXを向上し、ユーザーが使いやすいプロダクトを開発するために全力を注いでくれるエンジニアを募集しています。

career.up-sider.com

herp.careers

Kotlinでコルーチンの並行処理数を制限する

こんにちは!

UPSIDERのWebチームでサーバサイドKotlinを書いているエンジニアのおかだです。

突然ですが、コルーチンの並行処理数、制限したくなったことありませんか? 

ぼくはよくあります。

数千数万という数のコルーチンを同時に起動することもできる、と軽量さが強調されることもありますが、それはメモリ消費が少ないとかそういう話であって、コルーチンによって呼び出される側がその同時大量アクセスをさばけるかは別問題です。たとえば次のような制約が問題になることが時々あります。

特に、DBコネクションの取り扱いについてはKotlinコルーチンならではの落とし穴があったりするので、また別の記事で取り上げたいと思います。

追記:書きました → KotlinコルーチンでDBアクセスするときのアンチパターン - UPSIDER Techblog


まずはシンプルに実装してみる

さて、コルーチン数制限、雑でよければいろいろ書きようはありますが、結構あちこちで必要になってくるのでライブラリ化しておきたいところ。とりあえず、なるべくシンプルに作ってみましょう。

class LimitedCoroutineScope(
    private val underlying: CoroutineScope,
    limit: Int
) : CoroutineScope by underlying {

    private val semaphore: Semaphore = Semaphore(limit)

    private fun <T> withPermit(block: suspend CoroutineScope.() -> T): suspend CoroutineScope.() -> T =
        {
            semaphore.withPermit {
                block()
            }
        }

    fun launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ): Job = underlying.launch(context, start, withPermit(block))

    fun <T> async(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> T
    ): Deferred<T> = underlying.async(context, start, withPermit(block))
}

suspend fun <T> limitedCoroutineScope(
    limit: Int,
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend LimitedCoroutineScope.() -> T
): T =
    withContext(context) {
        LimitedCoroutineScope(this, limit).block()
    }

使い方はこう。こちらもシンプルです。

fun doSomething() = limitedCoroutineScope(20, context = Dispatchers.IO) {
  val users = userRepository.getAll().map { user ->
    async { findExtraInfoOf(user) }
  }.awaitAll()
  // ...
}

説明の必要はあまりないと思いますが、一点だけ。3行目の async は、 CoroutineScope.async ではなく、 LimitedCoroutineScope クラスの async メソッドです。これは、ある型に対して同じシグネチャの拡張関数とメンバ関数が定義されている場合、メンバ関数が優先される、というKotlinの仕様に依拠しています。ちょっと紛らわしいかもしれない。

CoroutineContextを使った実用的な実装

実際のプロジェクトでコルーチンまわりを共通化しようとするとき、上記のように新しい型を作ってそこに機能をもたせるやり方だと拡張が難しくなります。 CoroutineScope ってネストさせることもよくあり、たとえば次のように書くだけで、コルーチン数制限はできなくなります。

fun doSomething() = limitedCoroutineScope(20, context = Dispatchers.IO) {
  withTimeout(60.seconds) {
    // thisはLimitedCoroutineScopeではないので、コルーチン数制限はできない
  }
}

そもそも解決の仕方がKotlinコルーチンの設計思想とマッチしてないんでしょうね、たぶん。同時処理数をセマフォで制限する、という点は問題なさそうなので、そこは踏襲しつつ、もっとKotlinコルーチンらしい設計に変えてみましょう。こういうときに使えそうなのが CoroutineContext です。

たとえば、UPSIDERでも使っているORMのExposed。コルーチンがサスペンドしてスレッドを失ってもDBトランザクションを保つためには、スレッドローカルではなくコルーチン側にトランザクションを紐付けておく必要があります。Exposedは CoroutineContext 内にトランザクションを保持することでそれを実現しています。トランザクションの途中で新しい CoroutineScope を起ち上げた場合も、 CoroutineContext なら親から子へ引き継がれるので、その処理は同じトランザクションの一部となります。

まず、セマフォをラップして、 CoroutineContext の要素( CoroutineContext.Element 型)として扱えるようにします。要素と言いましたが、 CoroutineContext は構成要素ひとつひとつが CoroutineContext でもあります。Compositeパターンですね。

internal data class CoroutineLimit(
    val limit: Int
) : AbstractCoroutineContextElement(CoroutineLimit) {

    internal val semaphore: Semaphore by lazy {
        Semaphore(limit)
    }

    companion object Key : CoroutineContext.Key<CoroutineLimit>

    override fun toString(): String = "CoroutineLimit($limit)"
}

続いて、 CoroutineLimitCoroutineContext に付け足してくれる CoroutineScope ビルダを作ります。 limit が0以下のときはコルーチン数制限はされない、ということにしておきます。

suspend fun <T> limitedCoroutineScope(
    limit: Int,
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
    val newContext = coroutineContext + context +
        (if (limit > 0) CoroutineLimit(limit) else EmptyCoroutineContext)
    return withContext(newContext, block)
}

最後に、 CoroutineLimit が見つかったらコルーチン数を制限するという機能付きのコルーチンビルダを、 CoroutineScope の拡張関数として用意します。最初の例と違ってわかりやすい名前にしてみました。

fun CoroutineScope.launchWithLimitedConcurrency(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = coroutineContext + context
    return this.launch(context, start, newContext.createLimitedBlock(block))
}

fun <T> CoroutineScope.asyncWithLimitedConcurrency(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = coroutineContext + context
    return this.async(context, start, newContext.createLimitedBlock(block))
}

private fun <T> CoroutineContext.createLimitedBlock(block: suspend CoroutineScope.() -> T): suspend CoroutineScope.() -> T =
    this[CoroutineLimit]?.let {
        {
            it.semaphore.withPermit {
                this.block()
            }
        }
    } ?: block

使い方は、 launchasync の関数名が異なるだけで、基本的に最初の例と同じです。この実装なら、 CoroutineScope入れ子にしても正しく動いてくれます。

fun doSomething() = limitedCoroutineScope(20, context = Dispatchers.IO) {
  val users = userRepository.getAll().map { user ->
    asyncWithLimitedConcurrency { findExtraInfoOf(user) }
  }.awaitAll()
  // ...
}

fun doSomething() = limitedCoroutineScope(20, context = Dispatchers.IO) {
  withTimeout(60.seconds) {
    repeat(100) {
      // 内側のCoroutineScopeにCoroutineLimitが引き継がれているので、
      // 並行処理数は期待通り20個までに制限される
      launchWithLimitedConcurrency { /* do something */ }
    }
  }
}

CoroutineLimit を共有しさえすれば、スコープが入れ子になっていなくても同じコルーチン数制限が適用されてしまう点は、役に立つ場合もあるかもしれない反面、自由度が高すぎてシステムの挙動が読めなくなる可能性がありそうです。 CoroutineLimitinternal としているのはそのためです。


宣伝

UPSIDERでは、成長企業のための法人カード「UPSIDER」と、すべてのBtoB取引でクレジットカードを利用できるビジネスあと払いサービス「支払い.com」を提供しています。

ぼくが所属するWebチームはもちろん、UPSIDERの開発組織のいたるところでエンジニアが足りません。いろんな新しい技術にもこれから挑戦していきます。

カジュアル面談もやっておりますので、少しでもご興味のある方は、ぜひご連絡ください!

career.up-sider.com

herp.careers