これからもっとおもろくなるRemitAidのドメインディープダイブ in ソフトウェア開発
こんにちは!
RemitAid 取締役CTOの@iTakacです。
今回の記事では、我々の「海外ラクヤス振込」の開発で直面した複雑なドメインロジックについて、なぜその設計を選んだのかという判断プロセスを共有したいと思います。単なる実装例ではなく、「こういう選択肢があって、こんな理由でこっちを選んだ」という思考過程をお届けできればと思います。
複雑な問題を関心ごとは何か、ドメインモデルの責務は何か、など分解して捉えて解決できたときは、まさにソフトウェアエンジニア冥利に尽きます。
事業内容のおさらい
私たちはB2Bにおいて国際送金に代わる新しい送金の形を実現するプロダクト「海外ラクヤス振込」を開発・運営しています。
外貨受取は現地で、円精算は日本で行うことで、実質的なクロスボーダー送金を回避して手数料を大幅に削減する仕組みです。

本来クロスボーダー送金における為替に係る手数料は%にすると大きくありませんが、高額な送受金が発生するB2B取引においては、無視できないほどに大きくなります。
また送金の手続きや入金の確認にはまだまだ紙や電話、FAXなどのやりとりが主流です。
金銭的な負担、工数的な負担を弊社のプロダクトは解決します。
私たちの関心ごとは何か
翻って我々が日々開発している上での関心ごと、ドメインモデルの正体について迫ります。
ズバリ原価構造と残高管理です!
このビジネスモデルから生まれる技術的課題の本質は、「お客様に見せる金額」と「実際に両替できる金額」の乖離をいかに管理するかです。
従来の決済サービスでは手数料は事前に明確ですが、私たちのサービスでは以下の要因により手数料が動的に決まります:
- 非同期的な原価発生:受取手数料の確定タイミングと両替実行タイミングの乖離
- 為替変動による手数料額の変動:手数料自体が為替の影響を受ける
- 残高管理による識別不可能性:同一通貨の取引が残高に混在し取引ごとに識別ができない
素直に実装すると顧客体験が犠牲になってしまうので、技術的な課題を解決しつつ顧客体験も向上させる、しっかりとトレードオンする設計が欠かせません。
原価構造の複雑性
fintechは決済金額に対してtake rate、いわゆるスプレッドを乗せることが一般的です。
例えばクレジットカード決済の場合、100万円の売上に対して加盟店手数料3.6%がかかり、実際には手元に96.4万円が入る、といった具合です。
私たちのサービスも外貨で受け取るときに、原価として受取手数料が発生しています。
しかしクレジットカードと異なり、クロスボーダーでの受け取りにおいては日本国内に精算するための両替という行為が必要となります。
ここで問題となるのが、原価の発生タイミングと確定タイミングのズレです。
受取時点での原価の不確実性
例えば10,000USDの請求に対して、受取手数料が引かれて9,996USDを額面でお客さまに表示してしまった場合、インボイスの金額と異なる(=両替できる額が目減りしている)ため、違和感を持ちます。
さらに複雑なのは、この受取手数料が通貨や金額によって異なることに加えて、為替の影響を受けることです。
フィリピンペソでの受取手数料がタリフ上7USDと記載があった場合、何フィリピンペソになるかは受け取るまでわからないのです。
送金頻度による原価率の変動
顧客の資金はクロスボーダーしないことで安価に精算可能であることが弊社サービスの最大の特徴です。
一方で、パートナーに滞留している資金は最終的に弊社へ戻していかなければなりません。
この際のクロスボーダー送金は、従来の金融機関の国際送金とは異なり、取引都度ではなく頻度をコントロールできます。
送金にも原価が発生するため、お客さま資金がある程度まとまったタイミングを見計らって送金指示をする必要があります。これによって原価率を下げることができます。
つまり、受取時点では確定しなかった原価が、送金頻度の判断によってさらに変動するという二重の複雑性があるのです。
残高管理の課題
残高管理も複雑です。
外貨ごとに残高を保持しますが、一度受け取った外貨は「どの取引から来たお金か」の識別ができません。例えるなら異なる色の水を同じバケツに注いだら混ざってしまうのと同じです。
取引Aの10,000USDと取引Bの5,000USDがバケツに加算されると15,000USDの両替はできますが、「取引Aの分だけ」を指定した両替ができません。
これが何を示すのか。
前述した受取手数料と相互干渉してロジックを複雑化させます。
例えば以下のような状況になっているとします:
- 取引A:10,000USDの入金、9,996USD(受取手数料4USD)が残高に加算
- 取引B:5,000USDの入金、4,993USD(受取手数料7USD)が残高に加算
この残高から12,000USDを両替したい場合、どのようにすれば良いでしょうか?
私たちはどう解決したか?
検討した選択肢と判断プロセス
この複雑な問題に対して、実はいくつかの選択肢を検討しました。
選択肢1: 取引単位での完全分離管理
取引A専用残高: 9,996USD
取引B専用残高: 4,993USD
一見綺麗に見える解決策でしたが、両替時に複数取引にまたがる場合の処理が複雑化します。
12,000USDの両替をするために「取引Aから9,996USD、取引Bから2,004USDを両替する」といった取引ごとの指定が必要となり、運用が非常に煩雑になります。
採用した解決策: 元本比率 + FIFO の組み合わせ
私たちはこの問題に対して元本比率による比例配分とFIFO(先入先出)を組み合わせることで解決しました。
FIFO部分: どの取引から消費するかの順序決定
例えば以下のような取引があるとします:
取引A(古): 10,000USD、元本比率0.9996
取引B: 5,000USD、元本比率0.9986
取引C(新): 3,000USD、元本比率0.9990
13,000USDを両替したい場合、FIFOにより:
取引A全額(10,000USD)を消費
取引Bから一部(3,000USD)を消費
元本比率部分: 各取引からの正確な原資計算
取引A分:10,000USD × 0.9996 = 9,996USD
取引B分:3,000USD × 0.9986 = 2,995.8USD
合計原資:12,991.8USDを両替
なぜ元本比率 + FIFO を採用したのか
採用理由は以下のとおりです。
- 消費順序の明確性: どの取引から消費するかが決定的に決まる
- 正確な原資計算: 元本比率により各取引の手数料率を正確に反映
- 実装の単純性: FIFOで順序決定、元本比率で金額計算というシンプルな分離
- 拡張性: 将来の複雑な取引パターンにも対応可能
このように元本比率とFIFOを組み合わせることで、どのような両替要求に対しても一意かつ正確な原資計算が可能になります!
今後の展望:さらなるワクワクが待っている
双方向送金への挑戦
現在は受取のみですが、今後は送金機能も追加予定です。そうすると新たな複雑性が加わります:
// 送金予約による残高仮押さえ
type ReservedBalance struct {
Currency Currency
ReservedAmount decimal.Decimal
ReservationID string
ExpiresAt time.Time
}
// 債務管理との統合
type DebtPosition struct {
CounterpartyID string
Currency Currency
NetPosition NetPosition // 正: 債権, 負: 債務
LastUpdated time.Time
}
設計戦略:
- 既存設計の拡張: 元本比率の概念を債務管理にも適用
- 新しい抽象化: 「資金プール」から「ポジション管理」への概念拡張
- 段階的移行: 既存機能を維持しながら新機能を追加
段階的にアプローチする理由は、リスク管理と学習効果、そしてビジネス継続性です。
運用しながら最適な設計を発見していくことが重要だと考えています。
設計判断の原則
今回の開発を通じて、個人的には設計判断として以下の思想が大事だと感じています。
- 顧客体験を損なわない技術的複雑性の隠蔽
- 精度保証とデータ整合性の確保
これまで語ったように原価を動かす変数が多いため、P/L管理のためのデータ構造や設計が複雑化しやすい一方で、ユーザー体験は入金 -> 両替 -> 精算と比較的シンプルです。この2つを両立することに技術者としての腕の見せ所があるように思います。
最後に
いかがでしたでしょうか?
複雑なドメインにおける設計は、技術的な最適解を求めるだけでなく、ビジネス要求、運用制約、将来の変化を総合的に考慮した判断が重要です。
そしてその判断が現時点で合っていても、半年後から見ると間違っていることもあるのがソフトウェア設計の辛い面白いところです笑
完璧な設計は存在しないからこそ、「なぜその判断をしたか」を明確にし、継続的に改善していくことが、真に価値のあるソフトウェア開発につながると考えています。
今後は双方向でのお金の流れ、原価構造、残高管理が必要になりさらに複雑化していくことが予想されますが、だからこそソフトウェアエンジニアとしての腕の見せ所でもあるので、個人的にはこれからの開発がワクワクしています!
Discussion