LayerX エンジニアブログ

LayerX の エンジニアブログです。

不確実性をぶっ飛ばせ!Fintech事業部における新規プロダクト開発とは

この記事は、6月から始まっている #LXベッテク月間 21日目の記事です。 前日の記事はざべすさんの「Fintech事業部の全力のBet Technologyの様子をお見せします」でした。

tech.layerx.co.jp

こんにちは、Fintech事業部でサービスの新規立ち上げをしているエンジニアの @takochuu です。 LayerXから三井物産デジタル・アセットマネジメント(以下、MDM)に出向し、エンジニアとして星飛雄馬ばりの全力投球で個人投資家向け新規サービスの立上げをしています。 今回は新規サービスであるプロジェクトZENIOKOSHIが具体的にどんな開発プロセスで開発されていっているのかをご紹介します。

Fintech事業部と今回語る新規プロダクトについてはこちらのPodcastにて紹介していますので、よければお聞きください。 open.spotify.com

開発に対する不確実性

我々が開発している「ZENIOKOSHI」は4月に行ったインセプションデッキにて作成されたコンセプトに基づいて開発を進めており、チーム構成としては6月末現在までは PdM: 1 / デザイナー: 1 / エンジニア: 3 という少人数で開発しています。

「ZENIOKOSHI」は受益証券発行信託を用いたSTOという仕組みを利用して証券をみなさまに販売するスキームですが、我々プロダクトチームはほぼ全ての人間の出自がWeb系企業なので正直「STO...?スペーストルネードオガワかな...」というところからスタートです。(実際は社歴が長いメンバーはちゃんと詳しいですw)

このように扱っている商品に馴染みが(個人的には)まだない状態、かつ金融プロダクトを立ち上げる際には守らなければならない法令や府令、システム要件など不明瞭な点は多岐に渡ります。 そんな状態からどのように実際のプロダクト開発までこぎ着けたのかを今回は詳しくご紹介しようと思います。

不確実性潰し: 法令・システム連携編

前述の通り、守らなければならない要件が多いプロダクトなのでいきなり要件定義・デザイン・開発を行ったとしても正しくシステムローンチすることはできません。 やみくもにコードを書いていっても手戻りは必至なので、守らなければならない法令の解像度を上げる必要がありました。

解像度を上げると言っても法令の隅から隅まで読んで各々が理解するというのも無理があるため、解釈の余地なく「やらなければサービスをローンチできない」部分と「解釈に依存する部分」に分けて制約の解像度を上げていくことにしました。

「やらなければサービスをローンチできない部分」についてはユーザーストーリーを用意して社内の有識者に確認してもらうプロセスを取りました。プロダクトチームでドラフトされたユーザーストーリーをコンプラ・業務担当者含めて確認を行い、要件定義を進めます。 また、同時に信託銀行さんや銀行さんなどシステムを連携させていただく会社さんにもお話を始め、システム連携面での不確実性を下げていきました。 信託の解約は即時ではできないなどの制約があるため、具体的にフローチャートやシーケンス図で理解を進めていきます。

同時にプロダクトチーム内で職種関係なくワイヤーフレームを作成し、両方を成果物としてMTGでのレビューに回します。 レビューを経て、論点がなくなった画面・機能についてはいよいよ実装という流れになります。

↑ 実際に利用したユーザーストーリー

顧客に関する不確実性を下げるためにやったこと

法令にまつわる不確実性が下がったとは言え「結局顧客に受け入れてもらえるのか」というのが大事です。 俺が考える最強のシステムを思い込みで作っても顧客に受け入れられてもらえなければ失敗です。

こちら側の不確実性についてはユーザーヒアリングを概ね2回に分けて実施しました。 我々がメインターゲットとして定めた顧客層に対して、1回目はコンセプトについて。2回目はデザインのトンマナを比較するという内容でユーザー層の人たちにヒアリングし、有用な仮説を抽出しながら進めています。

どちらの解像度向上についても、メイン担当が居てその人が全部やるというわけではなく、同じ仕事をチーム全体で分担して全体の解像度を底上げすることを狙いにしています。

こちらはユーザーヒアリングに利用している現状のデザイン案なのですが、デザイン案があることでヒアリングもより具体的に実施できました。 (デザイン案を最初に見た時は一気にメンバーのテンションが上がりましたw) ※今後変更される可能性があります

開発プロセス

こちらに図示されているのが開発メンバーが作成してくれた新規プロダクト開発のプロセスです。 仕様も一度で決まりきらないため、決まった範囲で開発を進めることとUX的な変更は複数回入ることを予め想定し、開発とデザインが行き来する部分があるのが新規開発特有ではないでしょうか。

また、データベースの設計を先んじて行うことでエンジニア間の共通認識を作り、バックエンド構築のイメージのすり合わせができました。 現状では実際に動くUIを持つ画面で完成したものはない進捗ですが、エンジニア内で共通認識を取るためにバックエンド優先で開発を進めており、今冬リリース予定です。

おわりに

最後までお読みいただきありがとうございました!MDMでは「あたらしくて、おもしろい!」をモットーに新規プロダクト開発を進めていますが、まだまだ手が足りているとは言えない状況です。

ご紹介させて頂いたプロダクト開発に興味があるアニマルなエンジニアの方はもちろん、カスタマーサポート責任者やデザイナーなどのエンジニア以外の職種も募集しています。 少しでも興味がある方はご気軽にご連絡ください!

Fintech事業部(MDM)の紹介資料 jobs.layerx.co.jp

カジュアルに話してみたい方はこちら! 個人向けプロダクト開発チームのMeetyになります。 meety.net meety.net meety.net meety.net

求人への応募はこちら!

【FinTech】ソフトウェアエンジニア / 株式会社LayerX

【Fintech】デザイナー / 株式会社LayerX

【FinTech】カスタマーサポート責任者 / 株式会社LayerX

【FinTech】プロダクトマネージャー / 株式会社LayerX

Fintech事業部の全力のBet Technologyの様子をお見せします

この記事は、6 月から始まっている #LXベッテク月間 20 日目の記事です。

こんにちは、ざべすと申します。 僕はLayerXから三井物産デジタル・アセットマネジメント(以下、MDM)に出向し、エンジニアとしてアセットマネジメント業務のDXに取り組んでいます。 週末から異常な暑さが続いていますが、僕は先週から一足先に短パンとクロックスで出社しています笑 MDMは金融事業者ですがLayerXと変わらず好きな格好で出社できます(もちろんTPOはわきまえつつ....)

早いものでMDMも設立から2年が経ちました。2年も経てばプロダクトだけでなく業務効率化に関してもいろいろな芽が出てきます。 今回のLayerXのアドベントカレンダーのテーマはBet Technologyということで、MDMもLayerXに負けじと圧倒的にBet Technologyしているよという話をしたいなと思います。 今回は業務効率化への取り組みの近況報告と明日(!)リリース予定の社内業務効率化システム「DAM」の紹介の二本立てでお送りします!

圧倒的にBet Technologyするアセットマネジメント会社

MDMではアセットマネジメント業務のDXを会社の競争力の源泉の1つとして位置づけており、かなりのリソースを割いて日々業務改善に取り組んでいます。 過去に弊社丸野が書いた記事で我々がDXに力を入れる理由がわかりやすく解説されています。

tech.layerx.co.jp

今までには金融事業者に付きものである大量の稟議の効率化やファンドとして取得する物件選別の効率化システム「物件管理くん」の開発を行ってきました。物件管理くんについては過去に解説記事を書いたのでぜひお読みいただければと思います。

note.com

業務効率化チームの拡大

実はこの半年間で業務効率化チームは人数を大幅に拡大しました。 先述した業務効率化システム第一弾である「物件管理くん」の開発を始めた1年前はLayerX CTOの松本と僕の2人体制でした。 現在の業務効率化のシステム開発チームのエンジニアメンバーは5人で、かなり贅沢なメンバーが集まっています。

  • LayerX CTO松本
  • CTO経験者2名
  • セキュリティ、情シスのスペシャリスト
  • 元起業家(※僕)

プロ投資家向けプロダクトであるALTERNAや最近スタートした個人向けプロダクト(プロジェクト名: Zeniokoshi)の開発がある中、業務効率化にこれだけのリソースを割いています。 今回リリースするのはDAM(Digital Asset Management)というファンドの運用効率化のためのシステムで、このチームメンバーで半年間開発を進めてきました。

コンプラ部門も運用部門もみんなでBet Technology

単にエンジニアリソースを贅沢に割いているだけではなく、全ての部署でBet Technologyできているのも1つの特徴だと思います。 明日リリースするDAMの開発に当たって毎週のように仕様や使い勝手、個人情報の取扱や社内手続きなどコンプラの観点で問題がないかといった質問をさせてもらっていました。

MDMの強さの秘密はドメインエキスパートとの距離の近さにもあるのではないかなと日々感じています。

DAM(Digital Asset Management)のリリース

DAMとは

DAMとは何のシステムか。 一言で説明すると「物件の運用における定型業務をソフトウェアの力で効率化し、物件の収益改善により集中できるようになるシステム」です。 ファンド運用に関係する物件の契約情報や入出金情報、管理会社様とのコミュニケーションをDAMに集約していきます。

スクリーンショット①

スクリーンショット②

今回リリースする機能

今月のリリースによる業務のBefore Afterを図示すると下記のようになります。

今回のリリースのBefore After

弊社アセットマネジメント会社が運用している物件に新しく入居したい方が現れた場合、実際に賃貸借契約を締結するまでの流れは以下のようになります。

  1. 管理会社様(※図中のPMのこと。Property Management会社)から連絡を受ける
  2. 入居して問題ないか、反社チェックや賃料などの契約条件の確認
  3. 社内で承認後、管理会社様に入居して問題ない旨を連絡

とてもシンプルに見えますが定型的な作業が沢山発生します。

Before

管理会社様から入居申し込みを受け取った際のMDM社内手続きの流れは以下のとおりです。

  1. メールに添付されたパスワード付きZipの解凍
  2. 受け取った情報を反社チェックのシステムに手動で転記し、反社チェックを実施。証跡はGoogle Driveに保存。
  3. 社内のシステムで反社チェックの承認申請を実施し、社内の別の担当者に承認をもらう
  4. 入居申し込みの内容を稟議申請し、運用責任者に承認をもらう
  5. 管理会社様に入居申し込みが問題ないことを連絡

Before

1つの入居申し込みにつき5-10分程度の時間がかかっており、この作業だけで1日1時間以上かけている日もあります。1人で完結する作業ではないため、待ち時間も発生します。 今後MDMが運用する物件が増えるほどこれらの業務量も増えていくため、このままでは反社チェックと稟議をやっているだけで1日終わってしまうという状況になりかねません。

MDMでは入居申し込みの処理の効率化を喫緊の課題として考え、この半年間DAMの開発に取り組んできました。そして、ついに今回のリリースでこれらの業務はほぼ自動化されることになりました。

After

After

DAMを利用する場合、管理会社様がDAMに入居申請を入力すると反社チェックが自動で実施され、問題なかった場合には稟議データの作成まで自動で行われます。 つまり、MDMの運用担当者は今までの単純作業から開放され、DAMによって作成された稟議の内容を確認して社内申請ボタンを押すだけでよくなります。

ただし、最終確認だけは自動化せず、必ず担当者及び社内の責任者の判断を挟む運用になっています。

管理会社様に関しましても今までパスワード付きZipでメール送付していたものがDAMの入力フォームへの入力とアップロードだけで済むようになるほか、MDM社内での手続きが高速で完了することによってより素早く入居手続きに進むことができるメリットもあります。

DAMの今後の展望

今回のリリースでは大きな負担となっていた入居申し込みに関する業務の効率化のほか、今回は触れませんでしたが各物件の契約状況のデータベース化に取り組みました。

来月以降はこれらをベースにファンドの入出金管理や各物件のリーシング管理(入居者を増やして物件の稼働率を上げる活動の管理)、毎月の収支レポートの生成自動化などに取り組んでいく予定です。 リリースしてからがスタート、来月からも開発が楽しみです!

最後に

最後までお読みいただきありがとうございました!MDMは国内で最もBet Technologyしているアセットマネジメント会社だと自負していますが、もっともっとBet Technologyしていきたく、そのためにはもっともっと仲間が必要です。

MDMでは個人向けプロダクトの開発を中心に新しいメンバーを募集しています。 法的要件も絡む複雑な仕様策定からやっていきたいアニマルなエンジニアの方はもちろん、カスタマーサポート責任者やデザイナーなどのエンジニア以外の職種も募集しています。

少しでも興味がある方はご気軽にご連絡ください!

Fintech事業部(MDM)の紹介資料 jobs.layerx.co.jp

カジュアルに話してみたい方はこちら! 個人向けプロダクト開発チームのMeetyになります。僕のMeetyはこちら。 meety.net meety.net meety.net meety.net

求人への応募はこちら!

【FinTech】ソフトウェアエンジニア / 株式会社LayerX

【Fintech】デザイナー / 株式会社LayerX

【FinTech】カスタマーサポート責任者 / 株式会社LayerX

【FinTech】プロダクトマネージャー / 株式会社LayerX

創業4年のスタートアップが #技育博 に参加して感じてもらいたいこと

この記事は、6 月から始まっている #LXベッテク月間 19 日目の記事です。

前回は、「入社3ヶ月目のデザイナーから見たLayerXのデザインプロセス」という記事でした!すでに多くの方に読まれておりますが、まだご覧になっていない方は是非こちらもごらんください!

tech.layerx.co.jp

さて、今日は全国からエンジニア学生が集う「技育博 2022」に参加してきたお話を書きたいと思います。

渋谷にあるサポーターズさんのオフィスにて開催されたのですが、梅雨明け前にも関わらずなんと最高気温 35 度。いろいろな意味でアツい日になりました。

talent.supporterz.jp

技育プロジェクトとLayerX

サポーターズさんが主催している「技育プロジェクト」の理念に共感し、2022 年度の年間支援企業として LayerX も協賛させて頂いております。

技育(GEEK)プロジェクトは、 もの創りを行う学生を増やし、 未来の “技” 術者を “育”てる活動です。

LayerX はまだまだ立ち上がったばかりのスタートアップではありますが、行動指針の「徳」「Bet Technology」の観点から中長期的にエンジニアの育成にもコミットしていきたいと考えています。 こうした活動を通じ日本のエンジニアの裾野を拡げていくことで、持続可能な業界を作る一助となればと思っています。

技育プロジェクトでは、以下4つの機会を通じてインプットとアウトプットのサイクルを創出しており、今回はそのうちのひとつ「技育博」が行われました。

技育祭

実は、今年 3 月に行われた技育祭 2022 では代表取締役 CTO の @y_matsuiwtter「10〜1000名開発組織に向き合ったCTO 経験から語るキャリア論」というタイトルで登壇し、ログミーさんに全文を書き起こして頂いていました。

logmi.jp

技育 CAMP

また、技育 CAMP では 7/20(水) に「学生エンジニアのキャリアの積み上げ方 〜成長するエンジニア、今後求められるエンジニアとは〜」と題し、エンジニア学生向けの勉強会を行う予定です。

まだ申し込み可能ですので、是非こちらにもご参加ください!

技育博

今回の技育博は、意志を持ってオフライン開催という決断がなされていました。(コロナの感染状況を見つつ、スケジュールを確定していくのは難易度が高かったと思います。改めて運営の皆様ありがとうございます。)

全国から 80 の学生団体、約 200 名のエンジニア学生が集結し、各団体ごとにブースを出展する形式で行われ、各々の成果を発表したり、近況を報告したりとかなり盛り上がっていました。

LayerX からは代表取締役 CTO の松本 @y_matsuitter と HR の @serima の 2 名で参加しました。(学生だけでなく、他スポンサー企業さんともお話させて頂きましたが、CTO が来ているのは LayerX のみだった気がします)

個人的にも大規模なオフラインイベントへの参加はとても久しぶりで、学生やサポーター企業のリアルな熱気を感じることができ単純に楽しかったです。

なかには、社会人顔負けのプロダクトを作る学生や「好き」を突き詰めたハードウェアを持参した学生など、かなりバラエティに富み感心するばかりでした。

とある学生から「団体内に UI/UX チームが存在する」という話を聞いてとてもびっくりしました。頂いた名刺も洗練されていたりとまるで「イチ企業」だなと…。

学生のお話も聞きつつ、LayerX が何をやっている会社なのか?ということもお話させて頂き、興味を持ってくれた方もいたんじゃないかなと思います。(聞いてくれた皆さん、ありがとうございます。)

他社の人事担当の方ともお話させて頂く機会があったりと会社同士の横の繋がりもでき、とてもありがたかったです。

LayerXにおけるエンジニア新卒採用について

LayerX では長期インターンシップの受け入れを通年で行っており、インターンシップへの参画 → 新卒エンジニアとして入社、数名程度ではありますが過去実績があります。

そのうちの 1 名の kiyo がつい先日 note を書いてくれたので、是非読んでみて欲しいです。

入社 1 年目から大きな機能開発を任され、実際にエンドユーザとなるお客様に利用される経験は LayerX ならでは、だと思います。

note.com

受け入れられるキャパシティ的に、いきなり多くの学生を受け容れることはできないかもしれませんが、圧倒的な成長を望む学生にお会いしたいと考えています!

初めて参加して感じたこと・反省点

多くの学生、特に実際にアクションを起こしている方々とお話できたのはとても良かったと感じています。

熱量を感じることができたのはオフライン開催だったということもあるとは思いますが、実際に行動を起こしているエンジニア学生の説得力はひと味もふた味も違うなと改めて感じることが出来ました。

一方で LayerX としての反省点も多く、まさにここからだな!と身が引き締まる思いとなりました。

他社さんのブースや動きなど勉強になる点が多々あり、イベント当日の工夫の余地も数多くあったなと反省しております。次回に向けて色々とブラッシュアップしていきたいと考えています!

最後に

サポーターズ代表の楓さんが話していた内容からの引用にはなりますが、

『「良い会だった!楽しかった!」でおしまいじゃ、何も変わらない。出会った学生・企業に対して、是非一歩アクションを。』

学生時代の自分にも伝えたいなと感じた一言でした。

ぜひ、どんなに小さくても一歩、アクションを。話し足りなかった、話す機会がなかった方、是非こちらから!

meety.net

open.talentio.com

入社3ヶ月目のデザイナーから見たLayerXのデザインプロセス

この記事は、6月から始まっている #LXベッテク月間 18日目の記事です。

昨日の記事は弊社代表fukkyyさんの「LayerXの第3の事業、Privacy Tech事業を始めます、という話」でした。

note.com

はじめまして。SaaS事業部デザイナーのわたなべなつきです。

バクラクシリーズ担当のプロダクトデザイナーとして3月に入社し、早3ヶ月経過しました。

本日は入社3ヶ月目の比較的フレッシュな目線で、バクラクシリーズのデザイナーとしての働き方についてお話したいと思います。

まず入社して驚いたのは・・・

デザイナーがUIデザインしていない!

入社前から、どうやらデザイナーがUIデザインしてないらしい、ということは聞いていましたが、Figmaに一切ファイルがないので「本当にUIデザインしてないんだ・・・」とびっくりしました。

tech.layerx.co.jp

バクラクシリーズは基本的にデザインプロセスを挟まず開発をすることが多いです。

仕様が決まると、デザイナーがUIデザインをおこすことなく、エンジニアがUIコンポーネントを使って開発をしていきます。実装後、デザイナーも一緒にコードを修正しながらレイアウトやスタイルなどを調整します。

弊社は開発スピードが異常に早いのですが、このデザインプロセスもバクラクシリーズの爆速開発の理由の1つだな〜と入社してみて実感しました。

なので、いわゆる一般的な開発フローでUIデザインしてきた私はどうバリューを出していこうか・・・と最初の1ヶ月は悩んでいました。

が、まずは色々やってみよう!と思い、今はその時々に合わせたプロセスでデザインをしています。

コードを書きながらデザイン

これまで同様の「デザインプロセスを挟まない開発スタイル」に自分もチャレンジしてみました。

やってみて感じたのは、

  1. ロジックとスタイルの記述が分離されていてデザイナーが触りやすい。
    • フロントエンドはVueもしくはReact
    • HTML/CSSを触ったことのあるデザイナーなら触りやすい(JavaScriptは太古の昔にjQueryを書いた程度の私もイケました)
  2. エンジニア⇔デザイナーのコミュニケーションコストが減る
    • 「ここ10px空けたいです!」といったよくある会話が減る
  3. わからないことを聞く精神的なハードルが低いのでチャレンジしやすい
    • わからないことを快く教えてくれるエンジニアの方しかいないので、VueもReactも触ったことのなかった私としては大変ありがたいです。

ということで、やってみると面白く、業務の幅も広がりそうだなと感じました。

Figmaを使って議論の叩きをつくる

仕様が複雑だったり、新機能で考えることが多かったりする場合は、Figmaを使ったUIデザインも取り入れてはじめています(私がそっちのほうがやりやすかったのもあり・・・笑)。

先日リリースしたバクラク申請・経費精算のスマホ対応は、何案かプロトタイプを作ってチームでわいわい議論しながらUIを固めていきました。

実装後に大きくレイアウト変更するのは大変なので、みんなで議論しながらその場でUIデザインできるのはやはりFigmaの良いところだなと思います。

ちなみに、ComponentをLibrary化することで議論中にすばやくUIデザインできるようにしています。

余談ですが、私がFigmaを布教したらバクラク申請・経費精算のPdMもFigmaを使い始めています。フットワークが軽い方ばかりです。

まとめ

LayerXは「どう」作るか以上に「何を」「なぜ」作るかにこだわっている会社だなと入社してみて感じました。

なので、みんなやり方に固執せず「やりやすい方法でなんでもやってみなよ」と後押ししてくださる方ばかりです。

これからも試行錯誤しながらバクラクの体験をどんどん良くしていきたいと思います!

We are Hiring!!!

弊社では共に世の中をバクラクにしてくれる仲間を絶賛募集中です!

jobs.layerx.co.jp

先日LayerXデザイナー女子たちでPodcastも収録しました!LayerXのデザインチームの面白さが伝わると嬉しいです。 ぜひ聞いてみてください!

open.spotify.com

バクラクシリーズの DevOps チームの取り組み~CUJ/SLI 策定のご紹介~

この記事は、6月から始まっている #LXベッテク月間 13日目の記事です。
昨日は @michiru_da さんの 【すぐできる】LayerX カスタマーサクセスチームのBet Technology施策でした 📝 CS チームの Bet Technology な取組みが紹介されているので見てない方はぜひご覧ください!


こんにちは、LayerXで バクラクシリーズ のインフラを担当している DevOps チーム の多田(@tada_infra)です。私が所属する DevOps チームは今年から組成されたことで、チームとして取り組みを推進できるようになりました。この記事では DevOps チームの直近の取り組みをご紹介します。

DevOps チームの役割とロードマップの作成

私達はチームが組成された当初にチームの役割・達成したいことからクオーターごとのロードマップを策定し、各テーマごとの取り組みを書き出していきました。ロードマップを引いたことで直近自分たちがどんな課題と向き合っていくのか、改善していくのかが明瞭化されました。

チームの役割

  • サービスインフラの開発・運用
  • 開発チームの支援
  • Bizチームの支援

チームが達成したいこと

  • サービスの開発速度を落とさない(アウトカムの最大化)
  • インシデントの予防(組織の人数が増えてきたのでオペミス等が起こらないように改めて体制やオペレーションの見直し)
  • お客様の体験を損なわない

直近クオーターのロードマップ図抜粋

お客様の体験を損なわないために

上述のように DevOps チームが達成したいことの中に「お客様の体験を損なわない」というのを掲げました。そのためにはシステムの現状を把握し、お客様に安定したサービス提供ができているかをウォッチする必要があります。システムの監視を入れているため、概況は把握できているのですが、特定の機能レベルのパフォーマンスやエラーレートのデータ収集といったことはできていませんでした。プロダクトの利用体験に影響する機能が安定しているかどうかを監視し、開発チームや社内の関係者にこのデータを提供していくことでプロダクトの開発に活かせる状態をチームとして目指していくことにしました。そこで、私達はこの目的を達成していくための手法として Service Level Objective(以下、SLO) を設定し、システムの現状を計測・運用していくことで、「お客様の体験を損なわない」状態の実現を図っていくことにしました。

チーム内の取り組みより抜粋

CUJ と SLI の検討

はじめに、バクラクシリーズにおける Critical User Journey(以下、CUJ)がどこかを選択するディスカッションしました。各プロダクト毎のユーザー体験で重要な機能ってどこだろうというところから各プロダクト毎にお客様の体験に影響を与えている部分を話し合い、ここで列挙した機能ごとに指標となる Service Level Indicator(以下、SLI) を決めていきました。また、チーム内での議論を踏まえ、開発チームに各プロダクトごとの指標を展開してフィードバックをもらいました。

CUJ と SLI 考慮過程抜粋

自チーム内の議論やフィードバックを踏まえて、プロダクト毎に指標に関するデータの収集と可視化を行っていってます。弊社では Datadog を監視に利用しており、Datadog の SLO 機能や各種ログ、メトリクスを活用してプロダクトごとの指標データをダッシュボードに表示していきました。各プロダクト毎にダッシュボードを整えつつ、表示したデータの経過を見ながら SLO の具体的数値を定めていく活動を今後実施していきます。

ダッシュボードイメージ例

まとめ

DevOps チームの直近の取り組みの中でも CUJ/SLI の検討過程について紹介させていただきました。SLO の数値を決めて実運用を行うのはこれからですが、バクラクシリーズを利用いただいているお客様の体験向上に資するデータを提供していけるようブラッシュアップしていきます。その模様もまた記事に書いていきます。

なお、本記事でご紹介した活動においては、下記のブログや資料を参考にさせていただきつつ進めました。

medium.com

We are Hiring!!!

弊社では共に世の中をバクラクにしてくれる仲間を絶賛募集中です!

open.talentio.com

open.talentio.com

LayerXの組織的スピード感

初めまして! バクラク事業部でバクラク申請・経費精算の開発を担当している id:kikuchy です。
この記事は6月から始まっている #LXベッテク月間 10日目の記事です。

前回は@sh_komineさんによる 【GraphQL × Go】 N+1問題を解決するgqlgen + dataloaderの実装方法とCacheの実装オプション でした。
バクラクのバックエンドはまだ詳しくないのでとても勉強になりました。DataLoader活用していくぞい💪

tech.layerx.co.jp


id:kikuchy は2022年4月に弊社に入社しました。まだ入社から2ヶ月程度しか経過していません。
少々テクノロジーの話から逸れますが、エンジニアから見たこの会社がどんな会社なのか、その特徴を、気持ちがまだ新たなうちにお伝えしたいと思います。

バックグラウンド

  • スキル
    • スマートフォン向けアプリ開発が専門、中でもクロスプラットフォームフレームワーク(Flutter)
    • フロントからバックエンドまで必要あれば何でもやります
  • 前職
    • toCのデーティングアプリ開発
    • 7年くらいやってました

特徴

スピード感が圧倒的です。🏎️💨

前職がスピード感のない職場だったかと言えばそんなことは全く無いのですが、それにしてもLayerXのスピードが圧倒的なのです。

実装の速さもそうなのですが、何よりも意思決定の速度が早いのがスピード感の主な要因だと感じています。

どうして意思決定が早いのか

ベンチャースピリットとか、溢れんばかりのやる気、とかもあると思います。
が、個人の資質に左右される話は「○○さんだからできるんだよね」で終わってしまうので、この記事では構造的な要因に絞ってみました。

チームの小ささ

もしかしたら今のフェーズだけなのかも知れません。
もっと成長してもっと人が増えれば、また変わってくるかも知れません。

しかし、今の所、少人数制の体制が意思決定の速さにつながっているなと感じています。
人数の少なさは、情報共有の簡便さ。
バクラク申請・経費精算のチームの専任は4人で、何か話し合いがもたれるとしてもすぐに招集が可能。

人数の少なさは、目が行き届くということ。
適度なプレッシャーと、相談できる人が明確という安心感から、やることが決まってから実装までが早いのなんの。

やることが明確

業態がB2Bだからなのか、それともチームのマネジメント陣が優秀だからなのかはわかりません。多分両方でしょう。
新規のお客様のKnock-out Factor1や、既存のお客様からの要望が整理されていて、次にとりかかることが明確です。
またプロダクトのロードマップも丁寧に図示されている上に頻繁に更新されており、個々の施策がどんな意味を持ってくるのかもわかりやすく、実装にも気合を入れやすいのです。

エンジニアに求められるタスクの範囲の広さ

仕様の詳細もエンジニアがMTGに入って決めます。
ちょっとしたデザインくらいならエンジニアがアドリブで作ります。
要望についてわからないことがあれば社内のドメインエキスパートにインタビューすることはしょっちゅうですし、場合によってはお客様から直にヒアリングすることもあります。

エンジニアが決めてエンジニアが作る。早いのも道理ですね。

こうした働き方はこちらで榎本さんが語られている通りのものです。
ガツガツと一次情報を取りに(狩りに?)ゆくAnimalさを、普段から背中で語ってくれているのです。

tech.layerx.co.jp

上層の意思決定も早い

エンジニアだけでなく、他の職域の方々も意思決定が早いですし、マネジメント陣や経営陣も意思決定も早いです。
一週間の間に取りに行くマーケットの見直しなどが行われ、それを説明する図表まで合わせて書き換えられていることがあり、大変驚きました。

経営陣と全社員の距離が近い

100人とちょっとの従業員がいるのに、(傍目の印象ですが)全員と経営陣の距離が近いように感じます。

おそらく、以下の活動があるためにそう感じるのだと思っています。

  • 経営陣が「Feedback is a Gift」を掲げている
    • 実際に定期的に耳が痛いはずの意見を集め、それを全社で公開して今後はどうしてゆくのかを述べたり
  • 毎週の定例で経営陣が考えをプレゼンしている

実際に今年あった、耳が痛いはずな意見も吸い上げたリーダーシップサーベイ。その全社共有会があるのです

ざっくりまとめてしまうと、行動指針やカルチャーについて事あるごとに述べる、そして実践している姿勢を見せてくれているから、です。

物理的な距離の近さではなく、考え方に接することができること。
それが人間が感じる「近さ」なのだなと実感したものでした。

こうした経営陣の取り組みについて、松本さんが書かれた記事はこちら。
ここに書かれたことが有言実行されていることに驚きます。

tech.layerx.co.jp

We are Hiring!!

LayerXでは、一緒に世の中の経済活動をデジタル化してくれる人を大募集中です!

jobs.layerx.co.jp


明日はCSで大活躍のmichiru_daさんがCSチームのベッテクな事例を紹介してくださるそうです。 楽しみ〜〜〜!!


  1. お客様に弊社製品を導入していただく上で「この要素がないから導入できない」という要素のこと

【GraphQL × Go】 N+1問題を解決するgqlgen + dataloaderの実装方法とCacheの実装オプション

こんにちは。バクラク事業部でバクラク申請の開発を担当している@sh_komineです。 この記事は、6月から始まっている #LXベッテク月間 9日目の記事です。 前回はPrivacyTech事業部の@cipepserさんによる 合成データとは - 統計的な有用性を維持する架空のパーソナルデータ でした。 ものすごくBet Technologyな合成データのお話で読んでいてワクワクする記事です。気になる方は是非読んでみてください!


本日は、一般的なWebアプリケーション開発の技術で、バクラク事業部の開発で実際に使っているgqlgenとdataloaderの実装について紹介したいと思います。 gqlgen + dataloaderの記事自体は巷にだんだんと出揃ってきていると思いますが、弊社が使っている技術として改めてご紹介できたらと思います。

前提の話


今回の記事は自分が前回のエンジニアブログで書いた【GraphQL × Go】gqlgenの基本構成とオーバーフェッチを防ぐmodel resolverの実装で紹介できなかったDataLoaderの実装について紹介をしていきます。 tech.layerx.co.jp

gqlgenの基本的な使い方についてはこちらの記事から読まれることをお勧めします。

GraphQLの利便性とN+1問題とDataLoaderの役割


前段の記事でも触れていますが、GraphQLの大きな利便性はクライアント側から必要な時に必要なリソースを要求することができ、これにより不要なデータフェッチ(オーバーフェッチ)を防ぐことができることです。

オーバーフェッチ = リクエスト元で必要ないのに余分にリソースをフェッチしてしまうこと

少しおさらいすると、gqlgenではqueryで指定したtypeとgoで定義したstruct差分がある時にはmodel resolverが作成され、 queryに指定された時のみmodel resolverのメソッドが実行されるところまでご紹介しました。 (サンプルコードは前回に引き続き、一般的なTODOリストのアプリケーションです。)

graph/schema.graphqls

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User! # ⭐️ graphqlではUserを指定
}

type User {
  id: ID!
  name: String!
}

#  type Query: データフェッチ系のエンドポイント定義
type Query {
  todos: [Todo!]!
}

graph/model/models.go

package model

type Todo struct {
    ID   string `json:"id"`
    Text string `json:"text"`
    Done bool   `json:"done"`
    UserID *string `json:"user_id"` //  ⭐️ todoスキーマに持っているUserIDまで定義
}

todoの一覧取得でuserも取得するクエリ

query listTodos {
  todos {
    text
    done
    user {
      name
    }
  }
}

graph/schema.resolvers.go

func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
        // ⭐️ query指定があった時だけ、このメソッドが呼び出される
        r.userRepo.GetUserByTodoID(obj.ID)
}

// ...

// Todo returns generated.TodoResolver implementation.
func (r *Resolver) Todo() generated.TodoResolver { return &todoResolver{r} }

// ...

type todoResolver struct{ *Resolver }

前回きちんと触れることはできなかったのですが、上記の実装はN+1問題という有名な問題を抱えています。

N+1問題 = N件のレコードを取得した時、関連レコードの取得にN回別のフェッチを行い、合わせてN+1回のフェッチとなってしまうこと

今回の実装で具体的なシーケンス図を描くと以下のような流れになります。 TodoがN = 3件ある時、Userの取得フェッチは3回走ることとなります。 Todoの取得で1回、Userの取得で3回なので、N + 1 = 3 + 1ですね。これは非常に効率が悪い、、、。

(※ 今回はわかりやすさのため、データ取得をSQLクエリで書きましたが、現実ではマイクロサービスの別のAPIなどを呼び出すことも多いかと思います。)

DataLoader導入前のシーケンス図

ここにDataLoaderを導入するとこうなります。 (※ DataLoaderのクラスはもっと細かくありますが、ここでは簡略化して書いています。)

DataLoader導入後のシーケンス図

この図をみるとデータフェッチが激減していることがわかります。今回はN = 3のサンプルですが、N = 100などとなった時に、大きくパフォーマンスが変わることは想像に難くないです。 DataLoaderが各userResolverからのデータフェッチを一時受けして、一定時間待ち合わせた上でまとめて取得してきてくれるおかげで、N+1回のデータフェッチが2回のデータフェッチとなりました。 物凄いパフォーマンス改善です。画期的です。

このDataLoaderの技術はGraphQLの仕組みに欠かせない技術となっています。 ちなみに、DataLoaderの待ち合わせて読み込む仕組みを遅延読み込み(Lazy Loading)と言います。 遅延読み込みの説明は以下の記事がわかりやすく勉強になりましたので、ぜひご参照ください。感謝。

GraphQL と N+1 SQL 問題と dataloader - Qiita

dataloaderの実装サンプル


さて、具体的な実装サンプルもご紹介します。 golangのDataLoaderの実装はいくつかありますが、私たちのチームではgqlgenのreference「Optimizing N+1 database queries using Dataloaders — gqlgen」でも紹介されている graph-gophers/dataloader を利用しています。

今回はこちらのreferenceの実装方法を参考にしつつ、自分なりに少しわかりやすくリファクタしたものをサンプルとしました。

github.com

まず、dataloaderのuserの取得処理を実装したloader/user.goです。

package loader

import (
    "context"
    "fmt"
    "github.com/graph-gophers/dataloader"
    "github.com/shkomine/gqlgen-todos/graph/model"
    "github.com/shkomine/gqlgen-todos/repository"
    "log"
    "strings"
)

type UserLoader struct {
    userRepo repository.User
}

// BatchGetUsers dataloader.BatchFuncを実装したメソッド
// ユーザーの一覧を取得する
func (u *UserLoader) BatchGetUsers(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
    // dataloader.Keysの型を[]stringに変換する
    userIDs := make([]string, len(keys))
    for ix, key := range keys {
        userIDs[ix] = key.String()
    }
    // 実処理
    log.Printf("BatchGetUsers(id = %s)\n", strings.Join(userIDs, ","))
    userByID, err := u.userRepo.GetMapInIDs(ctx, userIDs)
    if err != nil {
        err := fmt.Errorf("fail get users, %w", err)
        log.Printf("%v\n", err)
        return nil
    }
    // []*model.User[]*dataloader.Resultに変換する
    output := make([]*dataloader.Result, len(keys))
    for index, userKey := range keys {
        user, ok := userByID[userKey.String()]
        if ok {
            output[index] = &dataloader.Result{Data: user, Error: nil}
        } else {
            err := fmt.Errorf("user not found %s", userKey.String())
            output[index] = &dataloader.Result{Data: nil, Error: err}
        }
    }
    return output
}

// LoadUser dataloader.Loadをwrapして型づけした実装
func LoadUser(ctx context.Context, userID string) (*model.User, error) {
    log.Printf("LoadUser(id = %s)\n", userID)
    loaders := GetLoaders(ctx)
    thunk := loaders.UserLoader.Load(ctx, dataloader.StringKey(userID))
    result, err := thunk()
    if err != nil {
        return nil, err
    }
    user := result.(*model.User)
    log.Printf("return LoadUser(id = %s, name = %s)\n", user.ID, user.Name)
    return user, nil
}

一つめのBatchGetUsersdataloader.BatchFunc を実装しています。

graph-gophers/dataloader/dataloader.go

// BatchFunc is a function, which when given a slice of keys (string), returns an slice of `results`.
// It's important that the length of the input keys matches the length of the output results.
//
// The keys passed to this function are guaranteed to be unique
type BatchFunc func(context.Context, Keys) []*Result

この dataloader.BatchFuncdataloader.NewBatchLoaderの第一引数に指定することで、dataloader.Loaderの初期化に利用します。 そしてdataloader.BatchFuncdataloader.Loadを一定待ち合わせた後に、dataloader.Loaderの中から呼び出されます。

このメソッドのメインの処理としては、userRepo.GetMapInIDsを呼び出していています。 このrepository/user.goは一般的なRepositoryなので詳しい説明は省略しますが、実装ではdbやapiからデータをフェッチする処理が書かれているイメージです。

repository/user.go

type User interface {
    GetByID(ctx context.Context, id string) (*model.User, error)
    GetMapInIDs(ctx context.Context, ids []string) (map[string]*model.User, error)
}

// ...

二つめのLoadUserメソッドはdataloader.LoadをUser用にwrapした関数で、todoResolverから以下のように呼び出します。 後述しますが、loaderの本体は context.Context にMiddlewareでInjectするので、呼び出し元からはシンプルにloader.LoadUserを呼び出すことができます。

schema.resolvers.go

func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
    if obj.UserID == nil {
        return nil, nil
    }
    user, err := loader.LoadUser(ctx, *obj.UserID)
    if err != nil {
        return nil, err
    }
    return user, nil
}

// ...

type todoResolver struct{ *Resolver }

次に、各ドメインのローダーを束ねて、contextにInjectするloader/loaders.goの実装です。

package loader

import (
    "context"
    "database/sql"
    "github.com/graph-gophers/dataloader"
    "github.com/shkomine/gqlgen-todos/repository"
    "net/http"
)

type ctxKey string

const (
    loadersKey = ctxKey("dataloaders")
)

// Loaders 各DataLoaderを取りまとめるstruct
type Loaders struct {
    UserLoader *dataloader.Loader
}

// NewLoaders Loadersの初期化メソッド
func NewLoaders(conn *sql.DB) *Loaders {
    // define the data loader
    userLoader := &UserLoader{
        userRepo: repository.NewUserRepo(conn),
    }
    loaders := &Loaders{
        UserLoader: dataloader.NewBatchedLoader(userLoader.BatchGetUsers),
    }
    return loaders
}

// Middleware LoadersをcontextにインジェクトするHTTPミドルウェア
func Middleware(loaders *Loaders, next http.Handler) http.Handler {
    loaders.UserLoader.ClearAll()
    // return a middleware that injects the loader to the request context
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        nextCtx := context.WithValue(r.Context(), loadersKey, loaders)
        r = r.WithContext(nextCtx)
        next.ServeHTTP(w, r)
    })
}

// GetLoaders ContextからLoadersを取得する
func GetLoaders(ctx context.Context) *Loaders {
    return ctx.Value(loadersKey).(*Loaders)
}

このサンプルではUserLoaderしかないですが、他のドメインの初期化もここに書かれます。 上記のloaderをmain関数の中でGraphQLに使うエンドポイントにInjectします。

server.go

package main

import (
    "database/sql"
    "github.com/shkomine/gqlgen-todos/loader"
    "github.com/shkomine/gqlgen-todos/repository"
    "log"
    "net/http"
    "os"

    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/playground"
    _ "github.com/go-sql-driver/mysql"
    "github.com/shkomine/gqlgen-todos/graph"
    "github.com/shkomine/gqlgen-todos/graph/generated"
)

const defaultPort = "8080"

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = defaultPort
    }

    // dbの初期化
    db, err := sql.Open("mysql", "root:password@tcp(127.0.0.1:13306)/test_db")
    if err != nil {
        log.Fatalf("main sql.Open error err:%v", err)
    }
    defer db.Close()

    // loaderの初期化
    ldrs := loader.NewLoaders(db)

    rslvr := graph.Resolver{
        TodoRepo: repository.NewTodoRepo(db),
        UserRepo: repository.NewUserRepo(db),
    }
    srv := handler.NewDefaultServer(
        generated.NewExecutableSchema(generated.Config{Resolvers: &rslvr}),
    )

    http.Handle("/", playground.Handler("GraphQL playground", "/query"))
    http.Handle("/query", loader.Middleware(ldrs, srv))

    log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

これを実際にGraphQL queryで呼び出してみると、、、。

query listTodos {
  todos {
    id
    text
    done
    user {
      id
      name
    }
  }
}

以下のように取得できました

{
  "data": {
    "todos": [
      {
        "id": "1",
        "text": "技術記事を書く",
        "done": false,
        "user": {
          "id": "1",
          "name": "田中 太郎"
        }
      },
      {
        "id": "2",
        "text": "実装をする",
        "done": false,
        "user": {
          "id": "2",
          "name": "佐藤 二郎"
        }
      },
      {
        "id": "3",
        "text": "日記を書く",
        "done": false,
        "user": {
          "id": "1",
          "name": "田中 太郎"
        }
      }
    ]
  }
}

なお、ソースコード上で仕込んだログは以下のように吐かれていて、きちんと待ち合わせて取得できていることがわかります。 DataLoaderめちゃくちゃ便利ですね。

2022/06/13 00:14:10 LoadUser(id = 1)
2022/06/13 00:14:10 LoadUser(id = 2)
2022/06/13 00:14:10 LoadUser(id = 1)
2022/06/13 00:14:10 BatchGetUsers(id = 1,2,1)
2022/06/13 00:14:10 return LoadUser(id = 1, name = 田中 太郎)
2022/06/13 00:14:10 return LoadUser(id = 1, name = 田中 太郎)
2022/06/13 00:14:10 return LoadUser(id = 2, name = 佐藤 二郎)

dataloaderのキャッシュの実装オプション


なお、今回は極力gqlgenのreference「Optimizing N+1 database queries using Dataloaders — gqlgen」通りに実装したのですが、キャッシュの実装は改善の余地があります。

graph-gophers/dataloaderに以下のような一文があり、DataLoaderには、http request単位の短いライフサイクルを考慮した簡単なCacheが実装されていることが書かれています。

This implementation contains a very basic cache that is intended only to be used for short lived DataLoaders (i.e. DataLoaders that only exist for the life of an http request). You may use your own implementation if you want.

it also has a NoCache type that implements the cache interface but all methods are noop. If you do not wish to cache anything.

https://github.com/graph-gophers/dataloader#cache

確かに NewBatchedLoaderのNewCache実装をみると、InMemoryCacheが使われていて、 mapsync.RWMutexを使ったシンプルな実装になっています。 このCacheは dataloader.Load関数内の最初に呼び出されており、Cacheから取得できたリソースはbatch処理で取得しにいきません。 サンプルの実装ではこの初期化時で生成したCacheをリクエストを跨いで使用しており、意図せぬ長い期間、リソースのキャッシュが残り続ける可能性があります。

graph-gophers/dataloader/dataloader.go

// NewCache constructs a new InMemoryCache
func NewCache() *InMemoryCache {
    return &InMemoryCache{
        items: &sync.Map{},
    }
}

// ...

// NewBatchedLoader constructs a new Loader with given options.
func NewBatchedLoader(batchFn BatchFunc, opts ...Option) *Loader {
    loader := &Loader{
        batchFn:  batchFn,
        inputCap: 1000,
        wait:     16 * time.Millisecond,
    }

    // Apply options
    for _, apply := range opts {
        apply(loader)
    }

    // Set defaults
    if loader.cache == nil {
        loader.cache = NewCache()
    }

    if loader.tracer == nil {
        loader.tracer = &NoopTracer{}
    }

    return loader
}

今回はキャッシュの実装オプションとして、以下の3通りのアプローチを紹介します。

  1. そもそもCacheを使わない
  2. Batch実行ごとにCacheをクリアする
  3. http requestごとにLoaderを生成をすることでCacheのライフサイクルを制御する
1. そもそもCacheを使わない

dataloader.NewBatchedLoaderメソッドでは、第二引数以降にOptionを渡すことができるのですが、dataloaderにはCacheを外から設定できる dataloader.WithCache メソッドが事前に定義されています。 キャッシュをしないdataloader.NoCacheも事前に定義されているので、これを設定することでキャッシュを無効化できます。 常に最新のデータを取りたい場合、確実に待ち合わせができるような実装であれば、これで十分かと思います。

loader/loaders.go

// NewLoaders Loadersの初期化メソッド
func NewLoaders(conn *sql.DB) *Loaders {
    // define the data loader
    userLoader := &UserLoader{
        userRepo: repository.NewUserRepo(conn),
    }
    loaders := &Loaders{
        UserLoader: dataloader.NewBatchedLoader(
            userLoader.BatchGetUsers,
            dataloader.WithCache(&dataloader.NoCache{}), // ⭐️NoCacheを設定する
        ),
    }
    return loaders
}
2. Batch実行ごとにCacheをクリアする

またオプション設定なのですが、Batch実行の度にキャッシュをクリアするオプションも提供されています。 この場合、リソースのキャッシュは同時に来たリクエストでは共有されます。 同時に複数のrequestが呼び出された時にキャッシュを共有したいが、あまりキャッシュを長く持ちたくない場合などには向いているかと思います。

loader/loaders.go

// NewLoaders Loadersの初期化メソッド
func NewLoaders(conn *sql.DB) *Loaders {
    // define the data loader
    userLoader := &UserLoader{
        userRepo: repository.NewUserRepo(conn),
    }
    loaders := &Loaders{
        UserLoader: dataloader.NewBatchedLoader(
            userLoader.BatchGetUsers,
            dataloader.WithClearCacheOnBatch(), // ⭐ Batch実行ごとにCacheをクリア
        ),
    }
    return loaders
}
3. http requestごとにLoaderを生成をすることでCacheのライフサイクルを制御する

最後に、requestごとにLoaderを生成する方法です。

GraphQLのN+1問題を解決する DataLoaderの使い方 - 一休.com Developers Blog

こちらの記事でも触れられているのですが、Loaderの初期化をリクエストごとに生成することで、リクエストごとに確実にCacheを初期化することができます。 弊社ではマルチテナントSaaSでrequestを跨いだキャッシュなどは持ちたくなかったことから、この方法で初期化をしています。

その場合はMiddleware内でLoadersの初期化を行うことで、確実にhttp request内に閉じた状態でCacheを持つことができます。

loader/loaders.go

// Middleware LoadersをcontextにインジェクトするHTTPミドルウェア
func Middleware(db *sql.DB, next http.Handler) http.Handler {
    // loaderの初期化
    ldrs := NewLoaders(db)

    // return a middleware that injects the loader to the request context
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        nextCtx := context.WithValue(r.Context(), loadersKey, ldrs)
        r = r.WithContext(nextCtx)

        next.ServeHTTP(w, r)
    })
}

キャッシュの実装オプションについて、簡単に3つ紹介してみました。 「1. そもそもCacheを使わない」や「2. Batch実行ごとにCacheをクリアする」で紹介したように dataloader.NewBatchedLoaderメソッドでは、第二引数以降にいろいろなOptionを渡すことができます。 今回紹介した以外にも、WithWaitWithInputCapacityWithBatchCapacityなどを使えばバッチの待ち時間や同時実行数、インプットの上限などでパフォーマンスチューニングしたりもできますので、是非使い倒して快適なGraphQLライフをお楽しみください。

We are Hiring!!


弊社では共に世の中をバクラクにしてくれる仲間を絶賛募集中です!

jobs.layerx.co.jp

自分のMeetyも公開していますので、少しでも興味を持ってくださった方は是非気軽に話を聞きに来てください!

jobs.layerx.co.jp