Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Modularising the Monolith - Laracon Online Winter 2022

Modularising the Monolith - Laracon Online Winter 2022

As a product and team grow, you might reach a point where it is hard to scale a monolith application due to different functionalities being tightly coupled in the same codebase with no boundaries.

Microservices have been making waves to solve such problems, however, they would bring their own sets of challenges.

A modular monolith can solve those problems without the drawbacks of microservices. A modular monolith is a system where all of the functionalities live in a single codebase and there are strictly enforced boundaries between different domains.

In this talk, I will walk you through how to modularise a monolith Laravel application based on domain boundaries and how to enforce strict boundaries between different domains.

Ryuta Hamasaki

February 09, 2022
Tweet

More Decks by Ryuta Hamasaki

Other Decks in Programming

Transcript

  1. 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
  2. BENEFITS OF MONOLITH - Single repository - Single CI/CD pipeline

    - Single set of infrastructure - Easy to handle DB transactions
  3. BIG BALL OF MUD - Tight coupling between functionalities -

    No domain boundaries - All code is globally accessible - Need to understand the entire codebase
  4. BENEFITS OF MICROSERVICES - Clear boundaries between services - Deployed

    independently - Can be scaled independently - Freedom of tech stack
  5. CHALLENGES OF MICROSERVICES - Multiple repositories - Multiple CI/CD pipelines

    - Network overhead - Complex cross-service transactions
  6. Contracts Implementation Tests Contracts Implementation Tests Contracts Implementation Tests Inventory

    service Order module Payment module INCREMENTAL MIGRATION TO MICROSERVICES
  7. MODULAR MONOLITH IS EASIER TO CHANGE Contracts Implementation Tests Contracts

    Implementation Tests Contracts Implementation Tests Inventory module Order module Payment module
  8. MODULES & TEAM STRUCTURE Order Module Payment Module Shipping Module

    Campaign Module Auth Module Inventory Module
  9. CONS OF MODULAR MONOLITH - Slow tests - More Git

    rebase - Cannot be scaled independently
  10. 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); } }
  11. 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); } }
  12. 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); } }
  13. 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); } }
  14. 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); } }
  15. 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); } }
  16. 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(); } }
  17. 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, ], ]; }
  18. 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); } }
  19. { "autoload": { "psr-4": { "App\\": "app/", "Laracon\\": "src/", "Database\\Factories\\":

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

    "database/factories/", "Database\\Seeders\\": "database/seeders/" } } } // composer.json
  21. Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart

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

    Item Order Order Line 2. Update inventory 3. Process payment 1. Checkout
  23. 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
  24. 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
  25. 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); } }
  26. 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); } }
  27. 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); } }
  28. 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); } }
  29. 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); } }
  30. 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); } }
  31. 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); } }
  32. 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); } }
  33. 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); } }
  34. 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); } }
  35. // src/Inventory/Contracts/ProductService.php namespace Laracon\Inventory\Contracts; interface ProductService { /** * Decrement

    product stock. */ public function decrementStock(int $productId, int $quantity): void; }
  36. 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); } }
  37. 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); } }
  38. 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); } }
  39. 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); } }
  40. 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); } }
  41. 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); } }
  42. 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; } }
  43. 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; } }
  44. DTO = Data Transfer Object - Simple data container class

    - Transfers data to another module - POPO (Plain Old PHP Object) - Has no behaviour
  45. 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, ); } }
  46. 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); } }
  47. 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); } }
  48. 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); } }
  49. 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; } }
  50. 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; } }
  51. 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; } }
  52. 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; } }
  53. 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); } }
  54. 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); } }
  55. namespace Laracon\Payment\Contracts; interface PaymentService { /** * Process payment for

    the given amount. */ public function charge(int $orderId, int $amount): void; }
  56. 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', ]); // ... } }
  57. 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')); }); } }
  58. 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); } }
  59. 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); } }
  60. 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); } }
  61. 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); } }
  62. 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); } }
  63. 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')); }); } }
  64. 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’)); }); } }
  65. 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
  66. 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
  67. 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); } }
  68. 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); } }
  69. 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); } }
  70. <phpunit> <testsuites> <testsuite name="Unit"> <directory suffix="Test.php">./src/**/Tests/Unit</directory> </testsuite> <testsuite name="Feature"> <directory

    suffix="Test.php">./src/**/Tests/Feature</directory> </testsuite> </testsuites> <coverage processUncoveredFiles="true"> <include> <directory suffix=".php">./src</directory> </include> <exclude> <directory suffix="Test.php">./src/**/Tests/*</directory> </exclude> <report> <clover outputFile="coverage/clover.xml"/> </report> </coverage> <!-- ... --> </phpunit>
  71. <phpunit> <testsuites> <testsuite name="Unit"> <directory suffix="Test.php">./src/**/Tests/Unit</directory> </testsuite> <testsuite name="Feature"> <directory

    suffix="Test.php">./src/**/Tests/Feature</directory> </testsuite> </testsuites> <coverage processUncoveredFiles="true"> <include> <directory suffix=".php">./src</directory> </include> <exclude> <directory suffix="Test.php">./src/**/Tests/*</directory> </exclude> <report> <clover outputFile="coverage/clover.xml"/> </report> </coverage> <!-- ... --> </phpunit>
  72. <phpunit> <testsuites> <testsuite name="Unit"> <directory suffix="Test.php">./src/**/Tests/Unit</directory> </testsuite> <testsuite name="Feature"> <directory

    suffix="Test.php">./src/**/Tests/Feature</directory> </testsuite> </testsuites> <coverage processUncoveredFiles="true"> <include> <directory suffix=".php">./src</directory> </include> <exclude> <directory suffix="Test.php">./src/**/Tests/*</directory> </exclude> <report> <clover outputFile="coverage/clover.xml"/> </report> </coverage> <!-- ... --> </phpunit>
  73. 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(); // ...
  74. 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(); // ...
  75. 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(); // ...
  76. CROSS-MODULE FOREIGN KEY LEADS TO A TIGHT COUPLING Order module

    Inventory module Cart Items Products product_id id
  77. 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(); // ...
  78. // ... $order = postJson('/order-module/orders', ['cart_id' => $cart->id]) ->assertCreated() ->json('data');

    assertDatabaseHas('orders', [ 'id' => $order['id'], ]); Event::assertDispatched(OrderFulfilled::class, $order['id']);
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. 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
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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
  94. 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
  95. 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