snaqme Engineers Blog

おやつ体験メーカー snaq.me エンジニアによる開発プロダクトブログ

FastAPI schemaの考え方

こんにちは
今回はFastAPIのschemaの考え方を書いていきたいと思います。

FastAPIではpydantic を採用しており、pydanticを利用してschema設定していきます。 今回でいうschemaはDBのschemaではなく、I/Oのリクエスト・レスポンスのschemaの考え方になります。

schemaを考えていくときに押さえていきたいこと

型の設定

昨今、型設定できない開発は開発スピード、不具合の原因、心理的安全性など多方面で良いことがないです。(型に縛られすぎるのも本末転倒なのでほどほどに) そのため、型設定できる方法があるのであれば、導入を考えたい。pythonでは mypy がありますので、mypyを用いて型のチェックをしていきます。

mypyは、プログラムの実行前に型チェックを行います。この型チェックはPythonの型ヒントという記法で書かれた情報をもとにしているので、型ヒントがない場合は、チェックがほぼ行われないので注意が必要です。

$ pip install mypy
box: int = 1
snaqme_price: dict[str, int] = {"box_fee": 1880, "delivery_fee": 330, "discount": 1000}

def total_delivery_fee(box: int) -> int:
    return snaqme_price["delivery_fee"] * box

このような int型やdict型などはさまざまなパターンで肩を決めることが可能です。 mypyに関しては別途詳しく書いていきます

ドメイン

schema管理する上でこのドメインで考えていくことが他の2つより大事だと考えています。 例えば、ユーザー情報を考えていく場合

・ 名前
・ メールアドレス
・ 住所
・ 電話番号
・ 決済情報
・ 生年月日
・ 好きなこと
・ 嫌いなこと
など

上記のような属性があった場合

ユーザーbase
・ 名前

ユーザー基本情報
・ メールアドレス
・ 住所
・ 電話番号
・ 決済情報
・ 生年月日

ユーザーの嗜好
・ 好きなこと
・ 嫌いなこと
など

のように分けられるかと思います。 この分け方が絶対と言いたいわけではなく、ユーザー情報という何も考えず一つのスキーマとして管理し続けるとどんな情報が入っているのかぱっと見わからなくなってしまいます。ユーザー情報 としている部分も将来的には分割が必要になり、肥大化されていく予想がつきます。 そのために細かく分けすぎず、大きく分けすぎずが良いと考えています。データベースと違い、FastAPIのレスポンスなどのスキーマは変更しやすいので、気軽にスタートできるのも特徴の一つになります。

レスポンスとリクエストの区別

スキーマとして管理できているがレスポンスとリクエストを根本から分けていくことで開発を進めやすくなると思います。ここで疑問になってくるのが、レスポンスとリクエストに関して同様なスキーマが存在しする場合どうするか?というところです。

私の考えでは無理に共通化させない、レスポンスはレスポンス、リクエストはリクエストと分ける。同様なものを共通化することは大事になってくる部分は多いと思いますが、レスポンスとリクエストに関しては共通化することで動作に不具合が生じさせる一つの原因になりかねない可能性があると考えています。
理由としては、レスポンスとリクエストは思いのほかコンテキストスイッチが発生しうるためです。コンテキストスイッチが発生する時にスキーマを考えていく場合どうしても片方のスキーマの存在を忘れがちになると思います。

なので、極力共通化しなくても良いと考えています。

まとめ

ざっくりではありますが、FastAPIでのschemaの考え方を共有させていただきました。 schemaの考え方に正解は存在しませんが、アンチパターンは存在しています。アンチパータンは開発スピードや心理的安全性を高められないため、アンチパターンが存在していると思っています。 いろんなschemaの考え方があると思いますので、ぜひ、いろんな方のスキーマの考え方を共有してもらえるとありがたく思います。

弊社では 「おやつと、世界を面白く。」 一緒に面白くしたい仲間をお待ちしてます!!

meety.net

engineers.snaq.me

FastAPI 独自でエラーハンドリングを設定する方法

こんにちは スナックミー CTO の三好 (@miyoshihayato) です
FastAPIを開発するときに独自でエラーハンドリングを設定する方法を書いていきたいと思います。

FastAPIの採用背景は以下をご覧ください

labs.snaq.me

リプレイスの場合、エラーハンドリングはリプレイス前の仕様と同じにする必要があり、少なからずレスポンスはコントロールできる状態が必要でした。リプレイスではなく初めからFastAPIを採用したという方でも、日本語に対応できるのだろうか?エラーコードってどういう仕様なのだろうか?などカスタムしたくなるシーンは出てくると思います。

FastAPIのデフォルトのエラーハンドリング

パスがない場合

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}

Request URL http://127.0.0.1/hello_world

Response body

{
    "detail": "Not Found"
}

パラメータ不足時のエラー

from fastapi import FastAPI, Form
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str = Form(..., title="名前", description="名前")
    price: float = Form(..., title="金額", description="金額")
    is_snaqme: bool = None


@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.post("/items/{item_id}")
def update_item(item_id: int, item: Item):
    return {"item_name": item.name, "item_id": item_id}

Request URL http://127.0.0.1/items/1

Request body

{
    "name": "パイナップル",
    "is_snaqme": true
}

price の値が必要だがrequest bodyに入れない場合

Response body

{
    "detail": [{
        "loc": [
            "body",
            "price"
        ],
        "msg": "field required",
        "type": "value_error.missing"
    }]
}

バリデーションエラー

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
def read_items(q: Union[str, None] = Query(default=None, max_length=5)):
    results = {"items": [{"item_id": "snaqme"}, {"item_id": "otumame"}]}
    if q:
        results.update({"q": q})
    return results

q は最大文字数5文字のところ6文字で入れた場合

Request URL http://127.0.0.1/items?q=123456

Response body

{
    "detail": [{
        "loc": [
            "query",
            "q"
        ],
        "msg": "ensure this value has at most 5 characters",
        "type": "value_error.any_str.max_length",
        "ctx": {
            "limit_value": 5
        }
    }]
}

このような Response body になっています。detail 直下に loc (エラー箇所), msg (メッセージ), type (エラータイプ) で構成されています。バリデーション設定によりその他のエラーメッセージも表示されています。

一方で以下のような

{
    "error": {
        "code": 401,
        "message": "ログイン失敗しました"
    }
}

のようにエラーコードとメッセージだけ出したい場合どのように行うとよいのでしょうか?

独自定義したExceptionを利用

import traceback
from fastapi import Request, Response, status
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware

class CustomException(Exception):
    """カスタム例外"""

    # デフォルトを401エラーとする
    default_status_code = status.HTTP_401_UNAUTHORIZED

    def __init__(
        self,
        msg: str,
        status_code: int = default_status_code,
    ) -> None:
        self.status_code = status_code
        self.detail = {"error": {"code": status_code, "message": msg}}


class SystemException(Exception):
    """システム例外"""

    def __init__(self, e: Exception) -> None:
        self.exc = e
        self.stack_trace = traceback.format_exc()
        self.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
        self.detail = {
            "error": {
                "code": status.HTTP_500_INTERNAL_SERVER_ERROR,
                "message": "システムエラーが発生しました。",
            }
        }


class HttpRequestMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next) -> Response:
        try:
            response: Response = await call_next(request)
        except CustomException as ce:
            # カスタム例外
            response = JSONResponse(ce.detail, status_code=ce.status_code)
        except Exception as e:
            se = SystemException(e)  # カスタムシステム例外に変換
            response = JSONResponse(se.detail, status_code=se.status_code)

        return response

CustomException のようにさまざまなパターンで例外処理を作成することが可能です。
呼び出し方は以下のようになります

from config.middleware import CustomException, HttpRequestMiddleware
from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Union[str, None] = Query(default=None, max_length=5)):
    if q != "snaq.me":
        rails CustomException(msg=f"not {q} but snaq.me")
    return {"Hello": "World"}

app.add_middleware(HttpRequestMiddleware)

まとめ

このようにエラーハンドリングを自由にカスタムできチームとして思い通りのエラーハンドリングの参考になれば幸いです。 またエラーハンドリングはレスポンスに特化しましたが、バリデーションもFastAPI既存の仕様に従うかどうかでやり方が変わってきます。 次回以降もFastAPIで培ってきたこと発信します。

弊社では 「おやつと、世界を面白く。」 一緒に面白くしたい仲間をお待ちしてます!!

meety.net

engineers.snaq.me

スナックミーのエンジニアチームで直近動きたいこと (2022年7月)

こんにちは スナックミー CTO の三好 (@miyoshihayato) です
四半期ごとにチームに共有している直近動いていきたいことを一部変えてこちらのエンジニアブログに載せたいと思います。

DBに残らないデータの活用

今回チームにとってしっかり取り組んでいきたいテーマがこちらになります。
弊社は商品開発・ユーザーさんに届ける商品の選定・製造/出荷効率 (リパック以外に自社でおやつも作ってます)など大事な役割が多く存在しています。これらの質の向上や施策の成功確度を高めるためにデータの活用は必須です。
今までも上記に関係するデータを活用し商品開発などは行なっていますが、活用しきれない・できないデータがいくつもありました。昨年から少しずつデータ基盤を構築していき今まで気づけなかったファクトに気づけ、捉えることができなった事象の把握・施策の実施をできるようになってきています。 (メンバーに感謝です)。
このデータ基盤は既存のDBをベースに整えてきてきました。次なるステップとして、DBに保存されないデータをしっかり活用していける基盤を作ることになります。アクセスログ・滞在時間・ヒートマップなど様々ありますが、スナックミーだからできることを考えて構築していきたいと考えています。

例えば、CRMについて。CRMは施策に対して効果があったかどうかというのはメールなどの open rateやclick rate が一般的になるかと思います。
確かに、open rateやclick rateは大事で悪いというわけではありません。ここで伝えたいことはその先で存在する意図したアクションをしてくれているのかどうなのかというのは実は分かりません。
施策を打ったが実は解約に繋がっていた、クリックしたのに何もせず直帰した、評価率向上に寄与したかったがリクエストの数も一緒に増えたなどネガティブ面もあれば、ポジティブな面の発見を総合的に判断できるようになります。

スナックミーだからできるアプローチを考え実行していきたいと考えています。

内製の強化 (+ 内製必要ないところの脱却)

2022年7月現在 社員 7名 + 業務委託 5名 計12名で開発をしているのですが、いくつか内製して開発しています。

  • 商品管理 (原料の発注〜出庫、製造、パックの数)
  • 商品の自動選定・出荷作業 (音声ピッキング)
  • ユーザーさんが利用するマイページ (サブスク、ECのような仕組み)
  • メールやLINEをセグメント配信するCRM
  • データ基盤

など. 必要に応じて開発をし、どの機能もスナックミーをワークさせるために大きな役割を果たしています。
ここでいう内製の強化というのは、上記システムの更なるアップデートになります
0 -> 1で作ったシステムや 1 -> 10 としてきたシステム様々ありますが、1 -> 10, 100 にするべきものはスケールアップしていき、 0 -> 1  で開発したがSaasなどで代用し他の開発に注力できる場合は置き換える。
また、1 -> 10 にもできるが、100 にするにはいつもよりパワーが必要で他が疎かになる可能性があるものを見極める
今いるメンバーの開発を最大限に活かし、会社, 個としてインパクトを最大化できる観点を持ち続け開発に取り組みたいと考えています。

他チームとスクラム

スナックミーはいろんなチームが存在するためいい意味でそれぞれ独立して仕事に取り組んでいます。ですが、他のチームとエンジニアが組むことで新しいシナジーを生み出せています。(マイページによるユーザー体験からオペレーションの製造・出荷まで)
数年前はできなかった(うまくワークしなかった)取り組みを今はできるようになってきたりしているので、より一層力を入れていきたいと考えています。
おやつを製造してユーザーさんにお届けするサービスを展開しているスナックミーは エンジニアだけではいいシステムは作れず、他のチームが存在しスクラムを組むことで1 + 1 が2以上の力を発揮できるようになります。
そのため、他チームとスクラムすることは今までやってこなかったわけではなく、より一層大事にしたい & 以前までのチーム目標の表現方法を少し変えているだけで、「他チームとスクラム」は根幹であると考えています。

まとめ

今年も半分が過ぎ、個として、チームとして、会社としてできる幅は日を重ねるごとに大きくなっています。その中で今回3つ大事にしたいことを書きました。いつもと変わらない姿勢もあれば、今まで培ってきたことをさらに飛躍させるための取り組みもあります。
一つ壁を乗り越えると新しい景色 + 新しい壁が現れます。常にこの繰り返しかもしれません。しかしチームと乗り越えた壁をチームと一緒に見る新しい景色を見続けたいので、少しでも興味持った方はぜひお話ししたいです。

おやつと、世界を面白く。」 一緒に面白くしたい仲間をお待ちしてます!!

meety.net

engineers.snaq.me

FastAPI 採用経緯

こんにちは スナックミー CTO の三好 (@miyoshihayato) です
スナックミーは昨年からPHP -> FastAPIにReplaceをすすめ、すでに多くのAPIがFastAPIで本番稼働しています。

ここではなぜReplaceしようと考えたのか、FastAPIを採用したのかなどを記載させていただければと思います。

Replaceしようと考えた理由

  • 高学習コスト (メジャーではないPHPフレームワークの利用のため)
  • コードの質 (スピードを意識しすぎて コードの質を担保できなくなった 当時は 速 ≠ 質 と勘違い )
  • 心理的安全性 (テスト含めリファクタの難易度が高い)

などの理由からReplaceを決断し様々なフレームワークを調査検討し始めました

高学習コスト

メジャーではないPHPフレームワークの利用のため、新しいメンバーが入る時に

のため学習コストが高くなる可能性があったため

では、なぜPHPフレームワークを最初に導入したのかというと、立ち上げ当初は慣れたアセットで行う方がプロダクトが検証しやすいなどの理由から上記のフレームワークを導入しました。

コードの質

立ち上げ当初は1週間単位で方向性・仕様が変わる(市場検証など)ため、ひとまず動くものを作り続けていました。そのため、設計に大きな時間を使わず1に開発、2に開発、3に開発するという状況でした。

またスピードを重視するあまり、質のことをほぼ考えておらず、中長期にスピード低下する危険性などは意識していませんでした。

心理的安全性

実際のコードはスパゲッティコード状態、テストの記入なし、変更の影響範囲が分かりにくいなどから改修・追加開発をするときに難易度が高く、何か不具合があったときに問題解決に時間がかかることが予想してました。 というのもこのPHPのコードは私だけ把握するようにし、理由はPHPのReplaceは決まっている & このコードを理解するより他で稼働しているシステムを理解し追加・改修等に集中するためです。Replace促進するためあえての属人化にしてました。

Bule/Greenデプロイを導入してあった程度で、それ以外はこの開発をする際の心理的安全性は決して高いものではありませんでした。

FastAPIを採用した理由

  • 他のメンバーが学習コスト低くスタートできる
  • APIに特化したい (Railsのような多機能なFWはいらない)
  • パフォーマンスに優れている (レスポンス速度や処理)

などの理由からFastAPIにしました

他のメンバーが学習コスト低くスタートできる

昨今Rust, Go などを使った開発が主流になりつつありますが、

  • 誰も(しっかり)経験したことない言語
  • APIをReplaceするというプロジェクトであること

などの場合、学習コストをあまり当てたくないという理由でRust, Goは対象からなくしました。 その中で候補になった言語は、弊社で本番稼働しているPython, Ruby, JS, (PHP) となり、その中でもFastAPIの自由度の高さがありました。(シンプルでソースコード量が少なく、規制を弊社仕様にカスタムしながら開発できる) 自由度が高い = 複雑さが増す可能性がある ということですが、API開発という観点 & 開発する前に指針を概ね定めるという観点でカバーすると決めました。

※ 新規PJなどではしっかり用途にあった技術選定を行いますので、Rust, Goなど関係なく現時点の最善な選定をします。

APIに特化したい (view開発は求めてない)

今回はAPIのReplaceなので、API以外の機能は特にいくら優れていても加点はほぼないと割り切りました。

パフォーマンスに優れている

APIに関してはパフォーマンスの高さが大事だと考えており、ここでいうパフォーマンスとは

  • レスポンス速度
  • 高リクエストも捌ける

他の後押しポイント

  • FastAPIは2018年にスタートしとしたが github の stars 46.7k
    • (flask (2010年〜) -> 59.4k, django (2005年〜) -> 64.9k) ということで近年の中で勢いがある
  • mypy と Pydanticを組み合わせることで型対応も可能
  • Swagger が標準装備してあり、APIのドキュメント管理が自動化 (実はここほぼで決定打)

など

まとめ

いろんな角度から考察した上で、FastAPIを採用し 現在カバレッジは90%以上確保していたり、かなり安定した運用・開発ができReplaceに関しては今の所うまく行っていると考えています。 今後はFastAPIのドキュメントがまだまだ少ないというのが現状なので、弊社で培ってきた考え方(エラーハンドリング・Pydanticでのバリデーション・schemaなど)を定期的に共有していきたいと思っております。

弊社では 「おやつと、世界を面白く。」 一緒に面白くしたい仲間をお待ちしてます!!

meety.net

engineers.snaq.me

スナックミーのエンジニアチームで直近動きたいこと (2022年4月)

こんにちは スナックミー CTO の三好 (@miyoshihayato) です
四半期ごとにチームに共有している直近動いていきたいことを一部変えてこちらのエンジニアブログに載せたいと思います。

エンジニアのバリュー

S: Scale Out
N: Next
A: Agile
Q: Quality

今年から弊社はVPoEを設け CTOである私が技術面にフォーカスし、VPoE が組織面にフォーカスする体制になりました。 この背景や行っていることは別途紹介させていただければと思っています。 VPoEを作る前からエンジニアのバリューを作成したいとずっと思っており、これを機にバリューを作成しました。
ちなみにスナックミー全社では以下がバリューになってます。

[おもしろい!]
難易度や工数じゃなく「それっておもしろい?」を軸に“ワクワクファースト”で行動します。
おもしろいほうがやる気が出るし、お客様に喜んでいただけるから。

[やってみよ!]
ごちゃごちゃ言ってないで、“オーナーシップ”を持ってまずやってみよう。
[つぎにいかそう!]
失敗したっていい。学びがあれば儲けもの。
失敗を恐れずどんどんチャレンジしていきましょう。みんな”やさしい”から失敗したって大丈夫だよ!

を頭文字にしたバリューです。 snaqme.com

バリューは覚えやすく自分的に気に入っています。 ですが、このバリューは全社に向けてバリューであり、エンジニアの目線で行くと少し昇華する必要があると感じていました。理由としてエンジニアにとって少しイメージしずらいものにどうしてもなっていたりします。例えば「おもしろい」これは人によって何が面白いのかバラバラです。バラバラというのが問題なのではなく、必ずしもユーザーさんと直接紐づいていないケースがあります。SREだと仲間のエンジニアのパフォーマンス最大化のために動くこと多く、製造や出荷などの社内システムは社員やパートさんが利用するために開発するケースが多いです。最終的にはユーザーさんに紐づきますが、少し感覚距離が遠いことろにいます。
ということから「」をベースにエンジニア向けに昇華させたバリューを作りました。
以下がバリューになりsnaq.meの「SNAQ」それぞれの頭文字にしたバリューにしました

Scale Out

エンジニアリングで事業成長を引っ張っていけるような開発をしていこう。その為にもスケールしやすい仕組みの開発を心がけよう。

Next

日々の挑戦の中で失敗をしてしまった時にでも、次への一歩が踏み出しやすい仕組みづくりを心がけよう。壊しやすい仕組みだったり、最速で開発・改善していけるような環境を持ち続けよう。

Agile

小さく早くスピード感を持って、継続的に改善を積み重ねていこう。

Quality

ユーザーにとって一番良いものを作っていこう。


  • スピードを重視したいが、短期的なスピードではなく中長期的なスピードも重視したい
  • 如何に小さく始められ
  • 失敗しても次に活かせるような仕組み
  • 速さだけでなくシステムの質も忘れない

このあたりをイメージしやすいようなバリューにしてます。

詳しいことは以下に

note.com

グロースハック

バリューを作ったので、バリューを意識した動きにしていきたいですが、もう一つテーマを決めたいなと思い共有しました。
それがグロースハックです。なぜ、グロースハックにしたのかというと、エンジニアは言われたものを開発することは一つの価値ですが、エンジニア自身で価値を社内・社外に提供できるような存在になっていきたいと思っているからです。相互の目線で一つの価値を2倍にも3倍にもすることが可能であり必要だと思っています。どっちが上ではなくどんな価値が最大化されるのかエンジニア自身が考えを共有し結果ユーザーさんの満足度が上がったり、インパクトが上がるもの作り上げていきたいと思っています。

まとめ

今回はエンジニアのバリュー(Scale Out Next Agile Quality )はVPoEが筆頭に考えを共有し一つ形にしましたので、どんどん浸透させていきたいと思います。
また、このバリューに共感でき方はぜひ一度カジュアルに話しませんか? 実際どう体現させているのか、どんなことをしているのかなどお話しします。 おやつと、世界を面白く。」 一緒に面白くしたい仲間をお待ちしてます!!

meety.net

engineers.snaq.me

他ブランド含めた在庫管理について

こんにちは スナックミー CTO の三好 (@miyoshihayato) です
弊社ではsnaq.meというおやつの定期便を展開しているのですが、実はあまり知られていない「他ブランド含めた在庫管理」について今回紹介させてください。

当初のブランドの種類

他ブランド と聞いてそもそもsnaq.meに他ブランドがあることを知らない方でもいるのではないでしょうか?
サービス最初は下記のように1つで展開していました。(1つといいつつ常時100種類ほどの多ジャンルのおやつからユーザーさんごとにパーソナライズされお届けしています。)

f:id:snaqme-labs:20220301184910j:plain

ブランドの種類

そこから進化していき今はさまざまなBOXを選べるようになっています(お届け2回目から可能です)

  • 通常: 今まで通り常時100種類ほどの多ジャンルのおやつからお届け
  • オツマミー: お酒のおつまみにもぴったりなスナック
  • グラノーラ: スナックミーのパティシエが作るグラノーラ1袋(250g)、トッピングにぴったりのナッツ、ドライフルーツ、ミニクッキーなどを合計4種セレクトしてお届け
  • 焼き菓子: 定期便でお届けしている人気の焼菓子のみをセレクトしたBOX

f:id:snaqme-labs:20220301185432j:plain

それぞれのBOXが取り扱うおやつは一部共通になっています。
ざっくりイメージとして、それぞれのBOXとおやつの共通部分は以下のような感じです。少なからず何か一緒のおやつがある状況です。
それぞれに特化したおやつもあるものの適しているおやつもあるため以下のような管理になっています。 f:id:snaqme-labs:20220301191356j:plain

在庫の管理の仕方

では、今まではどのように管理していたのかというとシンプルに書くと以下通りです。単一エリア在庫管理 同じ在庫の数を見て判断 もしくは、同一商品を作成という形で行っていました。同一商品を別々なものとして扱う場合、ユーザーごとの嗜好データにブレが生じる可能性があります。なぜならデータ上別々のおやつと判断されるため、管理コストが格段に上がってしまいます。
また、ユーザー数が増加とともに出荷スペースの細分化の必要性が増してきます。なぜなら細分化していかないと物理的なスペース・1人あたりの出荷効率から1箇所で出荷できる数に限度があるため、出荷できる箇所を分ける必要になります。(最大出荷数の向上)

f:id:snaqme-labs:20220301191728j:plain

単一エリア在庫管理

単一エリア在庫管理の在庫数を見て判断する方針で問題になってくるのが、BOXとの関係が密結合な状態であるため並列処理ができず、直列の処理になってしまいます。つまりどういうことことかというと、1つの作業が終わらないと次の作業に移れない状況になるということです。例えば2つ在庫があり、2人で分ける場合、1人が終わらないと次の人に行けない状態。別々で管理できれば待つことなくそれぞれに1つずつ振り分けることが可能ですができないということです。また在庫管理したことがある方はイメージつくと思いますが、在庫の考え方として実在庫理論在庫の2種類用意して運用しているところが多いと思います。

  • 実在庫 : 実際に存在している在庫数
  • 理論在庫 : 購入などによる在庫を確保可能な数

この実在庫と理論在庫の関係上、在庫を確保後に次の作業を開始することは可能ですが在庫が同一の場所である時のみ運用成立できます。在庫が別々にある場合は、在庫管理を分ける必要があります。

複数エリア在庫管理

ということから、在庫を分担する仕組みを導入しました (複数エリア在庫管理)
在庫管理のSaasの拠点別在庫管理できるサービスを参考にし、拠点内で複数在庫管理させたい場合、拠点が複数ある場合両方に対応できるようにしたい思い、以下のような構成しました。決して、DBを分けマイクロサービスにすることはせず、1つのDB内で別々で管理できるようにしてます。

entity 拠点 {
id -- comment '拠点のid'
}

entity エリア {
id -- comment 'エリアのid'
}

entity 在庫拠点ーエリア {
----
拠点_id -- comment '拠点のid'
エリア_id -- comment 'エリアのid'
}

entity 在庫 {
----
在庫拠点ーエリア_id -- comment '在庫拠点ーエリアのid'
}

拠点||..|{在庫拠点ーエリア
エリア||..|{在庫拠点ーエリア
在庫||..|{在庫拠点ーエリア

f:id:snaqme-labs:20220301192615j:plain

  • 拠点 : 物理的に離れている (都道府県)
  • エリア : 同じ建物内に複数存在 (フロア別や部屋別)

ざっくり上記のようなER図で開発し、現在拠点は1つですが、エリアはすでに複数運用しています。

まとめ

複数エリア在庫管理を導入する前は毎日のようにエンジニアが何かしら修正をしたり、運用方法の注意点を説明したりなど、本質的じゃない動きをよく行なっていました。 オペレーション側も注意する項目が多く、神経を費やし心理的安全性は決して高い状態ではありませんでした。
一見同じおやつが別々のエリアに存在し複雑そうに見えていますが、実運用上あるおやつが全体で何個あるかは把握せず、そのエリアに何個あるのか把握できる大事なので、運用上の心理的安全性・ミス・出戻りなどはかなり減ってきました。
また、具体的に 単一エリア在庫管理から複数エリア在庫管理にどのように変更したのかは別の機会で共有させてもらえればと思います。
スナックミーではユーザーさんが利用する開発 (体験向上)以外に、今回の在庫管理を含めたWMSなども内製していますので、さまざまな観点から技術的チャレンジハードからソフトまでプロダクト開発が可能です。

より詳しいこと聞きたい方・一緒に働きたい方は 以下のmeety or twitterから連絡お待ちしてます。

meety.net

twitter.com

ユーザーさんにおやつを届けるまでのアーキテクチャ

こんにちは スナックミー CTO の三好 (@miyoshihayato) です
登壇などでユーザーさんにおやつを届けるまでのアーキテクチャを紹介させてもらったことはありましたが、ここではなかったので改めて紹介させていただきます。 過去紹介しているものからいくつかアップデートもしているので、その辺りも一緒にご紹介させていただければと思います。

snaq.meとは

おいしいマルシェおやつとワクワクを詰め込んでハッピーな「おやつ体験」をお届けします

農家さんが大切に育てたこだわりの素材が、生産者さんの手で美味しいおやつに。全国の生産者さんとお客さまを繋げ、体と心に優しく美味しいおやつを、ワクワクと共に届けます。おやつの時間を価値あるものに、人々の心と暮らしを豊かにします。

ユーザーさんにおやつを届ける概要

アサイン : ユーザーさんに届けるおやつを決めること

ユーザーさんのアクション(評価やリクエスト、好き嫌いなど)から弊社の独自のアルゴリズムを用いてアサインをしています

・評価: 4段階 (苦手~大好き)
・リクエスト: 必ず入るとは限らないが「このおやつ食べたい!」という意思表示
・好き嫌い: 苦手な原料や好きなジャンルなど

f:id:snaqme-labs:20220201194831j:plain

アサインはサービス当初全て手作業で行っていました。評価は当初からできるようにしていたので、各々の評価データを1件ずつ確認しながらアサインをしていました。さらにアサインするためにどの商品がアサイン可能なのか、どういう組み合わせがいいのかは担当者に依存するため属人性の高い業務でした。そのため当初からアサインの自動化はサービス当初を急務として開発を進めサービス開始3ヶ月でβ版の導入しました。
(ユーザーが増えれば増えるほどアサイン業務に時間が埋め尽くされるため。
ex) 1人1分で100人行うには100分、500人もやろうと思えば8時間強の業務)

アサインを支えるアーキテクチャ

f:id:snaqme-labs:20220207164054p:plain

① : アサインするおやつの順番決め
② : アサイン実行環境
③ : ユーザー情報の格納

① : アサインするおやつの順番決め

ユーザーごとに好き嫌いが異なるので、アサインした方がいい順番も異なってきます。そのため、ユーザーとおやつに点数をつけていくところになります。点数が高ければ、好きな可能性が高く、低いと好きな可能性が低いという感じです。

② : アサイン実行環境

アサインを実行環境であり、基本的にここは2パターンで実行されます。

  • 基幹システムからの実行
  • 定刻での自動実行

ここの実行環境ではアサイン登録はされず実行と結果のファイルだけ出力される仕組みになってます。また、弊社の独自のアルゴリズムが存在し、ユーザーごとに8品の商品をアサインを可能にしています。アサインできる条件はいくつかあり、実際にアサインを実行する前にさまざまな前処理を行なっています。例えば、在庫がなく棚卸しさせるおやつや製造しアサイン可能であればロケーションidを採番したりなどです。

ロケーションid: おやつがどの場所に存在させるか振る作業
在庫があれば全てアサイン可能ではなく、限られたSKUのもアサインをしています
常時100種類ほど

③ : ユーザー情報の格納

ここに関しては以下をご覧ください

labs.snaq.me

アサインの種類

  • snaq.me の定期
  • スイッチボックス
  • 追いシリーズ (翌日配送のsnaq.meやotuma.me)
  • otuma.me 定期
  • その他 (臨時対応)

当初はsnaq.meだけのアサインでしたが、さまざまなアサインのタイプにも対応できる仕組みになっています。(条件も異なっています。)

まとめ

アサインのシステムはver.1 -> ver.2 -> ver.3 -> ...とアップデートしてきました。このアップデートはユーザーさんの声やデータから導かれた課題に対して向き合い取り組んできたところになります。しかし、課題は山積みです。製造や出荷効率を上げつつ、ユーザーさんの満足度をあげるこの両輪をうまく回転させることで事業を推進していけると思っています。

より詳しいこと聞きたい方は 以下のmeety or twitterから連絡お待ちしてます。

meety.net

twitter.com