Slide 1

Slide 1 text

RYUTA HAMASAKI @avosalmon

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Verification Solutions Service Provider >1M Accredified documents 9 markets 3 industries >9M verifications >500 Active issuers A B O U T A C C R E D I F Y

Slide 4

Slide 4 text

Modularising the Monolith Laracon Online Winter 2022

Slide 5

Slide 5 text

SHIP THE PRODUCT TO THE MARKET FIRST

Slide 6

Slide 6 text

BENEFITS OF MONOLITH - Single repository - Single CI/CD pipeline - Single set of infrastructure - Easy to handle DB transactions

Slide 7

Slide 7 text

BIG BALL OF MUD - Tight coupling between functionalities - No domain boundaries - All code is globally accessible - Need to understand the entire codebase

Slide 8

Slide 8 text

MICROSERVICES HTTP HTTP HTTP

Slide 9

Slide 9 text

BENEFITS OF MICROSERVICES - Clear boundaries between services - Deployed independently - Can be scaled independently - Freedom of tech stack

Slide 10

Slide 10 text

CHALLENGES OF MICROSERVICES - Multiple repositories - Multiple CI/CD pipelines - Network overhead - Complex cross-service transactions

Slide 11

Slide 11 text

TECHNICAL DECISIONS ARE ALWAYS TRADE-OFF

Slide 12

Slide 12 text

OUR GOALS - Maximise teams’ productivity - Scalable architecture

Slide 13

Slide 13 text

OUR APPROACH - Modularise the monolith - De fi ne domain boundaries

Slide 14

Slide 14 text

MODULAR MONOLITH Contracts Implementation Tests Contracts Implementation Tests Contracts Implementation Tests Inventory module Order module Payment module

Slide 15

Slide 15 text

MODULAR MONOLITH Contracts Implementation Tests Contracts Implementation Tests Contracts Implementation Tests Inventory module Order module Payment module

Slide 16

Slide 16 text

Contracts Implementation Tests Contracts Implementation Tests Contracts Implementation Tests Inventory service Order module Payment module INCREMENTAL MIGRATION TO MICROSERVICES

Slide 17

Slide 17 text

DISTRIBUTED BIG BALLS OF MUD

Slide 18

Slide 18 text

MICROSERVICES ARE EXPENSIVE TO CHANGE HTTP HTTP HTTP

Slide 19

Slide 19 text

MODULAR MONOLITH IS EASIER TO CHANGE Contracts Implementation Tests Contracts Implementation Tests Contracts Implementation Tests Inventory module Order module Payment module

Slide 20

Slide 20 text

MODULES & TEAM STRUCTURE Order Module Payment Module Shipping Module Campaign Module Auth Module Inventory Module

Slide 21

Slide 21 text

CONS OF MODULAR MONOLITH - Slow tests - More Git rebase - Cannot be scaled independently

Slide 22

Slide 22 text

DEFAULT STRUCTURE

Slide 23

Slide 23 text

MODULAR STRUCTURE

Slide 24

Slide 24 text

MODULAR STRUCTURE

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

middleware(['api', 'auth:sanctum']) ->namespace('Laracon\Order\Application\Http\Controllers') ->group(function () { Route::apiResource('orders', OrderController::class); }); // src/Order/routes.php

Slide 31

Slide 31 text

namespace Laracon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot() { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', ‘order'); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }

Slide 32

Slide 32 text

namespace Laracon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot() { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', ‘order'); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }

Slide 33

Slide 33 text

namespace Laracon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot() { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', 'order'); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }

Slide 34

Slide 34 text

namespace Laracon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot() { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', ‘order'); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }

Slide 35

Slide 35 text

namespace Laracon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot() { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', ‘order'); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }

Slide 36

Slide 36 text

namespace Laracon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot() { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', ‘order'); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }

Slide 37

Slide 37 text

namespace Laracon\Order\Providers; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Laracon\Order\Application\Policies\OrderPolicy; use Laracon\Order\Domain\Models\Order; class AuthServiceProvider extends ServiceProvider { protected $policies = [ Order::class => OrderPolicy::class, ]; public function boot() { $this->registerPolicies(); } }

Slide 38

Slide 38 text

namespace Laracon\Order\Providers; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Laracon\Order\Domain\Listeners\HandleOrderShipment; use Laracon\Shipping\Contracts\Events\ParcelShipped; class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ ParcelShipped::class => [ HandleOrderShipment::class, ], ]; }

Slide 39

Slide 39 text

namespace Laracon\Order\Providers; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Route; use Laracon\Order\Application\Http\Middleware\SomeMiddleware; use Laracon\Order\Domain\Models\Order; class RouteServiceProvider extends ServiceProvider { public function boot() { Route::bind('order', function ($value) { return Order::with('orderLines')->findOrFail($value); }); $this->app[‘router']->aliasMiddleware('do-something', SomeMiddleware::class); } }

Slide 40

Slide 40 text

'providers' => [ // ... \Laracon\Inventory\Providers\InventoryServiceProvider::class, \Laracon\Order\Providers\OrderServiceProvider::class, \Laracon\Payment\Providers\PaymentServiceProvider::class, \Laracon\Shipping\Providers\ShippingServiceProvider::class, ], // config/app.php

Slide 41

Slide 41 text

{ "autoload": { "psr-4": { "App\\": "app/", "Laracon\\": "src/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" } } } // composer.json

Slide 42

Slide 42 text

{ "autoload": { "psr-4": { "App\\": "app/", "Laracon\\": "src/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" } } } // composer.json

Slide 43

Slide 43 text

COMMUNICATION BETWEEN MODULES

Slide 44

Slide 44 text

COMMUNICATION THROUGH CONTRACT Module A Module B Contract Implementation Downstream Upstream

Slide 45

Slide 45 text

CONTRACT-FIRST DEVELOPMENT - Encapsulates implementation details - Change implementation without changing downstream module - Mock the contract in testing

Slide 46

Slide 46 text

Module A Module B COMMUNICATION WITHOUT CONTRACT Downstream Upstream Implementation

Slide 47

Slide 47 text

Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart Item Order Order Line

Slide 48

Slide 48 text

Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart Item Order Order Line 1. Checkout

Slide 49

Slide 49 text

Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart Item Order Order Line 2. Update inventory 1. Checkout

Slide 50

Slide 50 text

Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart Item Order Order Line 2. Update inventory 3. Process payment 1. Checkout

Slide 51

Slide 51 text

Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart Item Order Order Line 2. Update inventory 3. Process payment 4. Save order 1. Checkout

Slide 52

Slide 52 text

Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart Item Order Order Line 2. Update inventory 3. Process payment 5. Notify warehouse 4. Save order 1. Checkout

Slide 53

Slide 53 text

class OrderController extends Controller { public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 54

Slide 54 text

class OrderController extends Controller { public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 55

Slide 55 text

class OrderController extends Controller { public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 56

Slide 56 text

class OrderController extends Controller { public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 57

Slide 57 text

class OrderController extends Controller { public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 58

Slide 58 text

class OrderController extends Controller { public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 59

Slide 59 text

class OrderController extends Controller { public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 60

Slide 60 text

class OrderController extends Controller { public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 61

Slide 61 text

namespace Laracon\Order\Domain\Models; class CartItem extends Model { // ... public function cart(): BelongsTo { return $this->belongsTo(Cart::class); } public function product(): BelongsTo { return $this->belongsTo(Product::class); } }

Slide 62

Slide 62 text

namespace Laracon\Order\Domain\Models; class CartItem extends Model { // ... public function cart(): BelongsTo { return $this->belongsTo(Cart::class); } public function product(): BelongsTo { return $this->belongsTo(Product::class); } }

Slide 63

Slide 63 text

// src/Inventory/Contracts/ProductService.php namespace Laracon\Inventory\Contracts; interface ProductService { /** * Decrement product stock. */ public function decrementStock(int $productId, int $quantity): void; }

Slide 64

Slide 64 text

namespace Laracon\Inventory\Infrastructure\Services; use Laracon\Inventory\Contracts\ProductService as ProductServiceContract; class ProductService implements ProductServiceContract { public function decrementStock(int $productId, int $quantity): void { $product = Product::find($productId); if (!$product) { throw new ProductNotFoundException($productId); } if ($product->stock < $quantity) { throw new OutOfStockException($productId); } if (!$product->is_active) { throw new InactiveProductException($productId); } $product->decrement('stock', $quantity); } }

Slide 65

Slide 65 text

namespace Laracon\Inventory\Infrastructure\Services; use Laracon\Inventory\Contracts\ProductService as ProductServiceContract; class ProductService implements ProductServiceContract { public function decrementStock(int $productId, int $quantity): void { $product = Product::find($productId); if (!$product) { throw new ProductNotFoundException($productId); } if ($product->stock < $quantity) { throw new OutOfStockException($productId); } if (!$product->is_active) { throw new InactiveProductException($productId); } $product->decrement('stock', $quantity); } }

Slide 66

Slide 66 text

namespace Laracon\Inventory\Providers; use Laracon\Inventory\Contracts\ProductService as ProductServiceContract; use Laracon\Inventory\Infrastructure\Services\ProductService; class InventoryServiceProvider extends ServiceProvider { public $bindings = [ ProductServiceContract::class => ProductService::class, ]; // ... }

Slide 67

Slide 67 text

use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 68

Slide 68 text

use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 69

Slide 69 text

use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $this->productService->decrementStock($cartItem->product_id, $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 70

Slide 70 text

use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $this->productService->decrementStock($cartItem->product_id, $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 71

Slide 71 text

namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Domain\Models\Product; class Order extends Model { // ... protected $orderLines = []; public function orderLines(): HasMany { return $this->hasMany(OrderLine::class); } public function addOrderLine(Product $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }

Slide 72

Slide 72 text

namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Domain\Models\Product; class Order extends Model { // ... protected $orderLines = []; public function orderLines(): HasMany { return $this->hasMany(OrderLine::class); } public function addOrderLine(Product $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }

Slide 73

Slide 73 text

DTO = Data Transfer Object - Simple data container class - Transfers data to another module - POPO (Plain Old PHP Object) - Has no behaviour

Slide 74

Slide 74 text

namespace Laracon\Inventory\Contracts\DataTransferObjects; class ProductDto { public function __construct( public readonly int $id, public readonly string $name, public readonly int $price, ) {} }

Slide 75

Slide 75 text

namespace Laracon\Inventory\Contracts; use Laracon\Inventory\Contracts\DataTransferObjects\ProductDto; interface ProductService { /** * Get product by product id. */ public function getProductById(int $productId): ProductDto; }

Slide 76

Slide 76 text

namespace Laracon\Inventory\Infrastructure\Services; use Laracon\Inventory\Contracts\DataTransferObjects\ProductDto; use Laracon\Inventory\Contracts\ProductService as ProductServiceContract; use Laracon\Inventory\Domain\Models\Product; class ProductService implements ProductServiceContract { public function getProductById(int $productId): ProductDto { $product = Product::find($productId); if (!$product) { throw new ProductNotFoundException($productId); } return new ProductDto( $product->id, $product->name, $product->price, ); } }

Slide 77

Slide 77 text

use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $this->productService->decrementStock($cartItem->product_id, $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 78

Slide 78 text

use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $this->productService->decrementStock($cartItem->product_id, $cartItem->quantity); $product = $this->productService->getProductById($cartItem->product_id); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 79

Slide 79 text

use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $this->productService->decrementStock($cartItem->product_id, $cartItem->quantity); $product = $this->productService->getProductById($cartItem->product_id); $order->addOrderLine($product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 80

Slide 80 text

namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Domain\Models\Product; class Order extends Model { // ... protected $orderLines = []; public function addOrderLine(Product $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }

Slide 81

Slide 81 text

namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Domain\Models\Product; class Order extends Model { // ... protected $orderLines = []; public function addOrderLine(Product $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }

Slide 82

Slide 82 text

namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Contracts\DataTransferObjects\ProductDto; class Order extends Model { // ... protected $orderLines = []; public function addOrderLine(ProductDto $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }

Slide 83

Slide 83 text

namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Contracts\DataTransferObjects\ProductDto; class Order extends Model { // ... protected $orderLines = []; public function addOrderLine(ProductDto $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }

Slide 84

Slide 84 text

use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { // ... $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 85

Slide 85 text

use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { // ... $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 86

Slide 86 text

namespace Laracon\Payment\Contracts; interface PaymentService { /** * Process payment for the given amount. */ public function charge(int $orderId, int $amount): void; }

Slide 87

Slide 87 text

namespace Laracon\Payment\Infrastructure\Services; use Stripe\StripeClient; use Laracon\Payment\Contracts\PaymentService as PaymentServiceContract; class StripePayment implements PaymentServiceContract { private StripeClient $stripe; public function __construct(string $stripeKey) { $this->stripe = new StripeClient($stripeKey); } public function charge(int $orderId, int $amount): void { $this->stripe->charges->create([ 'amount' => $amount, 'currency' => 'usd', ]); // ... } }

Slide 88

Slide 88 text

namespace Laracon\Payment\Providers; use Illuminate\Support\ServiceProvider; use Laracon\Payment\Contracts\PaymentService as PaymentServiceContract; use Laracon\Payment\Infrastructure\Services\StripePayment; class PaymentServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PaymentServiceContract::class, function ($app) { return new StripePayment(config(‘services.stripe.key')); }); } }

Slide 89

Slide 89 text

use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct( private ProductService $productService, ) {} public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $stripe = new StripePayment(config('service.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 90

Slide 90 text

use Laracon\Inventory\Contracts\ProductService; use Laracon\Payment\Contracts\PaymentService; class OrderController extends Controller { public function __construct( private ProductService $productService, private PaymentService $paymentService, ) {} public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $stripe = new StripePayment(config('service.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 91

Slide 91 text

use Laracon\Inventory\Contracts\ProductService; use Laracon\Payment\Contracts\PaymentService; class OrderController extends Controller { public function __construct( private ProductService $productService, private PaymentService $paymentService, ) {} public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $stripe = new StripePayment(config('service.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 92

Slide 92 text

use Laracon\Inventory\Contracts\ProductService; use Laracon\Payment\Contracts\PaymentService; class OrderController extends Controller { public function __construct( private ProductService $productService, private PaymentService $paymentService, ) {} public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 93

Slide 93 text

use Laracon\Inventory\Contracts\ProductService; use Laracon\Payment\Contracts\PaymentService; class OrderController extends Controller { public function __construct( private ProductService $productService, private PaymentService $paymentService, ) {} public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $this->paymentService->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 94

Slide 94 text

namespace Laracon\Payment\Providers; use Illuminate\Support\ServiceProvider; use Laracon\Payment\Contracts\PaymentService as PaymentServiceContract; use Laracon\Payment\Infrastructure\Services\StripePayment; class PaymentServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PaymentServiceContract::class, function ($app) { return new StripePayment(config(‘services.stripe.key')); }); } }

Slide 95

Slide 95 text

namespace Laracon\Payment\Providers; use Illuminate\Support\ServiceProvider; use Laracon\Payment\Contracts\PaymentService as PaymentServiceContract; use Laracon\Payment\Infrastructure\Services\PaddlePayment; class PaymentServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PaymentServiceContract::class, function ($app) { return new PaddlePayment(config(‘services.paddle.key’)); }); } }

Slide 96

Slide 96 text

Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart Item Order Order Line 2. Update inventory 3. Process payment 5. Notify warehouse 4. Save order 1. Checkout

Slide 97

Slide 97 text

Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart Item Order Order Line OrderFul fi lled Publish Subscribe 2. Update inventory 3. Process payment 4. Save order 1. Checkout

Slide 98

Slide 98 text

class OrderController extends Controller { // ... public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $this->paymentService->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }

Slide 99

Slide 99 text

class OrderController extends Controller { // ... public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $this->paymentService->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } OrderFulfilled::dispatch($order->id); return new OrderResource($order); } }

Slide 100

Slide 100 text

class OrderController extends Controller { // ... public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $this->paymentService->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } OrderFulfilled::dispatch($order->id); return new OrderResource($order); } }

Slide 101

Slide 101 text

namespace Laracon\Shipping\Providers; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Laracon\Order\Contracts\Events\OrderFulfilled; use Laracon\Shipping\Domain\Listeners\NotifyWarehouse; class EventServiceProvider extends ServiceProvider { protected $listen = [ OrderFulfilled::class => [ NotifyWarehouse::class, ] ]; }

Slide 102

Slide 102 text

namespace Laracon\Shipping\Domain\Listeners; use Laracon\Order\Contracts\Events\OrderFulfilled; class NotifyWarehouse { public function handle(OrderFulfilled $event) { // Notify warehouse system } }

Slide 103

Slide 103 text

TESTING MODULE

Slide 104

Slide 104 text

./src/**/Tests/Unit ./src/**/Tests/Feature ./src ./src/**/Tests/*

Slide 105

Slide 105 text

./src/**/Tests/Unit ./src/**/Tests/Feature ./src ./src/**/Tests/*

Slide 106

Slide 106 text

./src/**/Tests/Unit ./src/**/Tests/Feature ./src ./src/**/Tests/*

Slide 107

Slide 107 text

it('creates a new order', function () { Event::fake(); $user = User::factory()->create(); $cart = Cart::factory()->create(['user_id' => $user->id]); // create some other test data... mock(ProductService::class, function ($mock) use ($product) { $mock->shouldReceive('decrementStock') ->with($product->id, 1) ->once(); $mock->shouldReceive('getProductById') ->with($product->id) ->once() ->andReturn($product); }); mock(PaymentService::class) ->shouldReceive('charge') ->once(); // ...

Slide 108

Slide 108 text

it('creates a new order', function () { Event::fake(); $user = User::factory()->create(); $cart = Cart::factory()->create(['user_id' => $user->id]); // create some other test data... mock(ProductService::class, function ($mock) use ($product) { $mock->shouldReceive('decrementStock') ->with($product->id, 1) ->once(); $mock->shouldReceive('getProductById') ->with($product->id) ->once() ->andReturn($product); }); mock(PaymentService::class) ->shouldReceive('charge') ->once(); // ...

Slide 109

Slide 109 text

it('creates a new order', function () { Event::fake(); $user = User::factory()->create(); $cart = Cart::factory()->create(['user_id' => $user->id]); // create some other test data... mock(ProductService::class, function ($mock) use ($product) { $mock->shouldReceive('decrementStock') ->with($product->id, 1) ->once(); $mock->shouldReceive('getProductById') ->with($product->id) ->once() ->andReturn($product); }); mock(PaymentService::class) ->shouldReceive('charge') ->once(); // ...

Slide 110

Slide 110 text

CROSS-MODULE FOREIGN KEY LEADS TO A TIGHT COUPLING Order module Inventory module Cart Items Products product_id id

Slide 111

Slide 111 text

it('creates a new order', function () { Event::fake(); $user = User::factory()->create(); $cart = Cart::factory()->create(['user_id' => $user->id]); // create some other test data... mock(ProductService::class, function ($mock) use ($product) { $mock->shouldReceive('decrementStock') ->with($product->id, 1) ->once(); $mock->shouldReceive('getProductById') ->with($product->id) ->once() ->andReturn($product); }); mock(PaymentService::class) ->shouldReceive('charge') ->once(); // ...

Slide 112

Slide 112 text

// ... $order = postJson('/order-module/orders', ['cart_id' => $cart->id]) ->assertCreated() ->json('data'); assertDatabaseHas('orders', [ 'id' => $order['id'], ]); Event::assertDispatched(OrderFulfilled::class, $order['id']);

Slide 113

Slide 113 text

HOW CAN WE ENFORCE DOMAIN BOUNDARIES?

Slide 114

Slide 114 text

STATIC ANALYSIS - Deptrac - De fi ne layers based on PHP classes - De fi ne rulesets on layers access - Detect violation of the rulesets - Visualize dependency graph

Slide 115

Slide 115 text

parameters: paths: - ./app - ./src layers: - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: App: - Laravel Contract: - Laravel imports: - src/Inventory/deptrac.yaml - src/Order/deptrac.yaml - src/Payment/deptrac.yaml - src/Shipping/deptrac.yaml // deptrac.yaml

Slide 116

Slide 116 text

parameters: paths: - ./app - ./src layers: - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: App: - Laravel Contract: - Laravel imports: - src/Inventory/deptrac.yaml - src/Order/deptrac.yaml - src/Payment/deptrac.yaml - src/Shipping/deptrac.yaml // deptrac.yaml

Slide 117

Slide 117 text

parameters: paths: - ./app - ./src layers: - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: App: - Laravel Contract: - Laravel imports: - src/Inventory/deptrac.yaml - src/Order/deptrac.yaml - src/Payment/deptrac.yaml - src/Shipping/deptrac.yaml // deptrac.yaml

Slide 118

Slide 118 text

parameters: paths: - ./app - ./src layers: - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: App: - Laravel Contract: - Laravel imports: - src/Inventory/deptrac.yaml - src/Order/deptrac.yaml - src/Payment/deptrac.yaml - src/Shipping/deptrac.yaml // deptrac.yaml

Slide 119

Slide 119 text

parameters: paths: - ./app - ./src layers: - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: App: - Laravel Contract: - Laravel imports: - src/Inventory/deptrac.yaml - src/Order/deptrac.yaml - src/Payment/deptrac.yaml - src/Shipping/deptrac.yaml // deptrac.yaml

Slide 120

Slide 120 text

parameters: paths: - ./app - ./src layers: - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: App: - Laravel Contract: - Laravel imports: - src/Inventory/deptrac.yaml - src/Order/deptrac.yaml - src/Payment/deptrac.yaml - src/Shipping/deptrac.yaml // deptrac.yaml

Slide 121

Slide 121 text

parameters: paths: - ./app - ./src layers: - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: App: - Laravel Contract: - Laravel imports: - src/Inventory/deptrac.yaml - src/Order/deptrac.yaml - src/Payment/deptrac.yaml - src/Shipping/deptrac.yaml // deptrac.yaml

Slide 122

Slide 122 text

parameters: paths: - ./app - ./src layers: - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: App: - Laravel Contract: - Laravel imports: - src/Inventory/deptrac.yaml - src/Order/deptrac.yaml - src/Payment/deptrac.yaml - src/Shipping/deptrac.yaml // deptrac.yaml

Slide 123

Slide 123 text

parameters: paths: - ./app - ./src layers: - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: App: - Laravel Contract: - Laravel imports: - src/Inventory/deptrac.yaml - src/Order/deptrac.yaml - src/Payment/deptrac.yaml - src/Shipping/deptrac.yaml // deptrac.yaml

Slide 124

Slide 124 text

parameters: paths: - ./src/Order layers: - name: OrderModule collectors: - type: bool must: - type: className regex: Laracon\\Order\\.* must_not: - type: className regex: Laracon\\Order\\Contracts\\.* - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: OrderModule: - App - Contract - Laravel // src/Order/deptrac.yaml

Slide 125

Slide 125 text

parameters: paths: - ./src/Order layers: - name: OrderModule collectors: - type: bool must: - type: className regex: Laracon\\Order\\.* must_not: - type: className regex: Laracon\\Order\\Contracts\\.* - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: OrderModule: - App - Contract - Laravel // src/Order/deptrac.yaml

Slide 126

Slide 126 text

parameters: paths: - ./src/Order layers: - name: OrderModule collectors: - type: bool must: - type: className regex: Laracon\\Order\\.* must_not: - type: className regex: Laracon\\Order\\Contracts\\.* - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: OrderModule: - App - Contract - Laravel // src/Order/deptrac.yaml

Slide 127

Slide 127 text

parameters: paths: - ./src/Order layers: - name: OrderModule collectors: - type: bool must: - type: className regex: Laracon\\Order\\.* must_not: - type: className regex: Laracon\\Order\\Contracts\\.* - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: OrderModule: - App - Contract - Laravel // src/Order/deptrac.yaml

Slide 128

Slide 128 text

parameters: paths: - ./src/Order layers: - name: OrderModule collectors: - type: bool must: - type: className regex: Laracon\\Order\\.* must_not: - type: className regex: Laracon\\Order\\Contracts\\.* - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: OrderModule: - App - Contract - Laravel // src/Order/deptrac.yaml

Slide 129

Slide 129 text

parameters: paths: - ./src/Order layers: - name: OrderModule collectors: - type: bool must: - type: className regex: Laracon\\Order\\.* must_not: - type: className regex: Laracon\\Order\\Contracts\\.* - name: App collectors: - type: className regex: App\\.* - name: Contract collectors: - type: className regex: Laracon\\[^\\]*\\Contracts\\.* - name: Laravel collectors: - type: className regex: (Laravel|Illuminate)\\.* ruleset: OrderModule: - App - Contract - Laravel // src/Order/deptrac.yaml

Slide 130

Slide 130 text

STATIC ANALYSIS IS 
 NOT PERFECT

Slide 131

Slide 131 text

USEFUL LINKS - https://github.com/avosalmon/modular-monolith-laravel - Deconstructing the Monolith: Designing Software that Maximizes Developer Productivity - Under Deconstruction: The State of Shopify’s Monolith - Long live the Monolith! Monolithic Architecture != Big Ball of Mud - Strategic Monoliths and Microservices - Domain-Driven Laravel

Slide 132

Slide 132 text

THANK YOU @avosalmon