Slide 1

Slide 1 text

Practical insight into modulithic architecture Max Beckers 𝕏 @maxbbeckers Elbphilharmonie Hamburg

Slide 2

Slide 2 text

Contact: 𝕏 @maxbbeckers [email protected] linkedin.com/in/maximilianbeckers Max Beckers Solutions Architect β€’ Working for / β€’ Living near Kiel, Germany β€’ Symfony Experience since 2012

Slide 3

Slide 3 text

Software architecture is always about finding the best solution for your use case and your application in your specific context. The challenge is to find the right balance between simplicity and flexibility. 𝕏 @maxbbeckers

Slide 4

Slide 4 text

Simplicity in focus Monoliths 𝕏 @maxbbeckers

Slide 5

Slide 5 text

Focusing on flexibility Microservices 𝕏 @maxbbeckers

Slide 6

Slide 6 text

Finding its place between Monoliths and Microservice Modulithic Architecture Modular Monolith 𝕏 @maxbbeckers

Slide 7

Slide 7 text

𝕏 @maxbbeckers Monolith – Modulith – Microservice – Overview Microservices Modulith Monolith Multiple Both possible Single Codebase Multiple Single Single Releases Multiple Single Single Deployments Domain / Business centered Domain / Business centered Technical design Design Principles Orchestration & Monitoring Releases / Coordination Understanding Code Complexity Network overhead Direct calls Direct calls Performance Services + Integration Modules + Integration Whole codebase Testing

Slide 8

Slide 8 text

𝕏 @maxbbeckers Monolith – Modulith - Microservice Microservices Modulith Monolith ❌ βœ” βœ” Simplicity βœ” ❌* ❌ Scalability βœ” βœ” ❌ Low Coupling βœ”* βœ”* ❌* Maintainability ❌ βœ” βœ” Single point of failure βœ” ❌* ❌ Free in technology usage ❌ βœ” βœ” Transaction Handling

Slide 9

Slide 9 text

Let's dive into the example … 𝕏 @maxbbeckers

Slide 10

Slide 10 text

𝕏 @maxbbeckers

Slide 11

Slide 11 text

𝕏 @maxbbeckers

Slide 12

Slide 12 text

𝕏 @maxbbeckers Architecture Overview API GATE WAY Payment Processing Order Management Customer Management SEPA Mandate Payment Provider

Slide 13

Slide 13 text

𝕏 @maxbbeckers Symfony Application Overview Use-Case based Structure instead of technical namespaces at the first level.

Slide 14

Slide 14 text

𝕏 @maxbbeckers Generate Code from the API #!/usr/bin/env bash openapi-generator-cli generate \ -i ./public-contract-definition.yaml \ -g php \ -o ./generated \ --additional-properties=\ invokerPackage=Worldline\\Payments\\Api\\V2,\ modelPackage=Models,\ artifactVersion=2.279.0,\ composerPackageName=worldline/payment-api,\ licenseName=MIT

Slide 15

Slide 15 text

𝕏 @maxbbeckers Composer part "repositories": [ { "type": "path", "url": "api-spec/generated", "options": { "symlink": true } } ], $ composer require worldline/payment-api:@dev ./composer.json has been updated Running composer update worldline/payment-api Loading composer repositories with package information Updating dependencies

Slide 16

Slide 16 text

𝕏 @maxbbeckers API Gateway "Internal" namespace denotes module internal functionality – cannot be accessed outside the module context.

Slide 17

Slide 17 text

𝕏 @maxbbeckers Modulith internal APIs - Objects Modules have data structures defined by DTOs

Slide 18

Slide 18 text

𝕏 @maxbbeckers Modulith internal APIs - Services Services in a module are called using interfaces. In a Microservices context, this could be the OpenAPI Spec.

Slide 19

Slide 19 text

𝕏 @maxbbeckers Modulith internal APIs - Messages Modules can consume messages defined in Message. namespace App\Order\Message; use App\Order\Dto\OrderDto; class PersistOrderEvent { public function __construct( private readonly OrderDto $orderDto ) { } public function getOrderDto(): OrderDto { return $this->orderDto; } }

Slide 20

Slide 20 text

𝕏 @maxbbeckers Modulith internal APIs - Overview β€’ DTOs – Data Structure for the API of the Module β€’ Interfaces – Endpoints for the API of the Module β€’ Messages – Async Communication API of the Module β€’ Exceptions – Error Responses of the Module

Slide 21

Slide 21 text

𝕏 @maxbbeckers #[Route(name: "app_v2_payments_")] class PaymentsController extends AbstractController { public function __construct( private readonly PaymentServiceInterface $paymentService, private readonly PaymentMapper $paymentMapper ) {} #[Route('/v2/{merchantId}/payments', name: 'create_payment', methods: ['POST'])] public function create(Request $request, string $merchantId): JsonResponse { /** @var CreatePaymentRequest $createPaymentRequest */ $createPaymentRequest = ObjectSerializer::deserialize($request->getContent(), CreatePaymentRequest::class); $paymentRequest = $this->paymentMapper->mapCreatePaymentRequestToPaymentDTO($createPaymentRequest, $merchantId); $paymentResponse = $this->paymentService->pay($paymentRequest); $response = $this->paymentMapper->mapPaymentDTOToPaymentResponse($paymentResponse); return $this->json(ObjectSerializer::sanitizeForSerialization($response), 201); } } Payments Controller

Slide 22

Slide 22 text

𝕏 @maxbbeckers #[Route(name: "app_v2_payments_")] class PaymentsController extends AbstractController { public function __construct( private readonly PaymentServiceInterface $paymentService, private readonly PaymentMapper $paymentMapper ) {} #[Route('/v2/{merchantId}/payments', name: 'create_payment', methods: ['POST'])] public function create(Request $request, string $merchantId): JsonResponse { /** @var CreatePaymentRequest $createPaymentRequest */ $createPaymentRequest = ObjectSerializer::deserialize($request->getContent(), CreatePaymentRequest::class); $paymentRequest = $this->paymentMapper->mapCreatePaymentRequestToPaymentDTO($createPaymentRequest, $merchantId); $paymentResponse = $this->paymentService->pay($paymentRequest); $response = $this->paymentMapper->mapPaymentDTOToPaymentResponse($paymentResponse); return $this->json(ObjectSerializer::sanitizeForSerialization($response), 201); } } Payments Controller

Slide 23

Slide 23 text

𝕏 @maxbbeckers #[Route(name: "app_v2_payments_")] class PaymentsController extends AbstractController { public function __construct( private readonly PaymentServiceInterface $paymentService, private readonly PaymentMapper $paymentMapper ) {} #[Route('/v2/{merchantId}/payments', name: 'create_payment', methods: ['POST'])] public function create(Request $request, string $merchantId): JsonResponse { /** @var CreatePaymentRequest $createPaymentRequest */ $createPaymentRequest = ObjectSerializer::deserialize($request->getContent(), CreatePaymentRequest::class); $paymentRequest = $this->paymentMapper->mapCreatePaymentRequestToPaymentDTO($createPaymentRequest, $merchantId); $paymentResponse = $this->paymentService->pay($paymentRequest); $response = $this->paymentMapper->mapPaymentDTOToPaymentResponse($paymentResponse); return $this->json(ObjectSerializer::sanitizeForSerialization($response), 201); } } Payments Controller

Slide 24

Slide 24 text

𝕏 @maxbbeckers Persistence & Data

Slide 25

Slide 25 text

𝕏 @maxbbeckers β€’ Each module has its own tables β€’ Modules are not integrated at the database level (SELECTS / JOINS using tables of other modules are not allowed) 4 ways of data isolation: β€’ Different type of persistence (e.g. MySQL, Postgres, Redis, MongoDB, …) β€’ Separate database for each module β€’ Separate schema for each module (used in the example) β€’ Separate tables in the same schema (I would not recommend) Persistence & Data Rules

Slide 26

Slide 26 text

𝕏 @maxbbeckers Database setup API GATE WAY Payment Processing Order Management Customer Management SEPA Mandate Payment Order Customer Mandate

Slide 27

Slide 27 text

𝕏 @maxbbeckers Entities

Slide 28

Slide 28 text

𝕏 @maxbbeckers Database config .env CUSTOMER_DATABASE_URL="postgresql://customer:[email protected]:5432/customer?serverVersion=16&charset=utf8" MANDATE_DATABASE_URL="postgresql://mandate:[email protected]:5432/mandate?serverVersion=16&charset=utf8" ORDER_DATABASE_URL="postgresql://order:[email protected]:5432/order?serverVersion=16&charset=utf8" PAYMENT_DATABASE_URL="postgresql://payment:[email protected]:5432/payment?serverVersion=16&charset=utf8" Can be different for each module Can be different for each module

Slide 29

Slide 29 text

𝕏 @maxbbeckers Database config doctrine: dbal: connections: customer: url: '%env(resolve:CUSTOMER_DATABASE_URL)%' mandate: url: '%env(resolve:MANDATE_DATABASE_URL)%' order: url: '%env(resolve:ORDER_DATABASE_URL)%' payment: url: '%env(resolve:PAYMENT_DATABASE_URL)%' default_connection: payment orm: entity_managers: customer: connection: customer mappings: Customer: type: attribute is_bundle: false dir: '%kernel.project_dir%/src/Customer/Internal/Entity' prefix: 'App\Customer\Internal\Entity' alias: Customer

Slide 30

Slide 30 text

𝕏 @maxbbeckers Doctrine Migrations #config/packages/migrations/migrations_customer.yaml em: customer migrations_paths: App\Migrations\Customer: 'migrations/Customer' table_storage: table_name: migration_versions $ php bin/console doctrine:migrations:diff --em=customer --configuration=config/packages/migrations/migrations_customer.yaml Generated new migration class to "migrations/Customer/Version20240502123126.php" To run just this migration for testing purposes, you can use migrations:execute --up 'App\\Migrations\\Customer\\Version20240502123126' To revert the migration you can use migrations:execute --down 'App\\Migrations\\Customer\\Version20240502123126'

Slide 31

Slide 31 text

𝕏 @maxbbeckers What about queues?

Slide 32

Slide 32 text

𝕏 @maxbbeckers Messenger setup API GATE WAY Payment Processing Order Management Customer Management SEPA Mandate Payment Order Customer

Slide 33

Slide 33 text

𝕏 @maxbbeckers Queue config .env CUSTOMER_MESSENGER_TRANSPORT_DSN=amqp://customer:password@localhost:5672/customer/messages ORDER_MESSENGER_TRANSPORT_DSN=amqp://order:password@localhost:5672/order/messages PAYMENT_MESSENGER_TRANSPORT_DSN=amqp://payment:password@localhost:5672/payment/messages framework: messenger: transports: customer: dsn: '%env(CUSTOMER_MESSENGER_TRANSPORT_DSN)%' order: dsn: '%env(ORDER_MESSENGER_TRANSPORT_DSN)%' payment: dsn: '%env(PAYMENT_MESSENGER_TRANSPORT_DSN)%' routing: 'App\Customer\Message\PersistCustomerEvent': customer 'App\Order\Message\PersistOrderEvent': order 'App\Payment\Internal\Message\PersistPaymentEvent': payment

Slide 34

Slide 34 text

𝕏 @maxbbeckers Scaling Consumers with Supervisor $ sudo apt-get install supervisor ;/etc/supervisor/conf.d/messenger-worker-payment.conf [program:messenger-consume] command=php /home/www/bin/console messenger:consume payment --time-limit=3600 user=ubuntu numprocs=2 startsecs=0 autostart=true autorestart=true startretries=10 process_name=%(program_name)s_%(process_num)02d

Slide 35

Slide 35 text

𝕏 @maxbbeckers Flexibility API GATE WAY Payment Processing Order Management Customer Management SEPA Mandate

Slide 36

Slide 36 text

𝕏 @maxbbeckers Add a new module API GATE WAY Payment Processing Order Management Customer Management SEPA Mandate Invoicing

Slide 37

Slide 37 text

𝕏 @maxbbeckers Modulith is an architectural alternative between Monolith and Microservices β€’ Simplicity & Maintainability β€’ Low coupling & clear boundaries β€’ Less complexity Monolith: Understanding code, Testing, Clear boundaries Microservices: Easier to configure, no network overhead The Complexity of the business code itself stays of course πŸ˜‰ Summary

Slide 38

Slide 38 text

𝕏 @maxbbeckers Don’t start with microservices β€œYou shouldn't start a new project with microservices, even if you're sure your application will be big enough to make it worthwhile.” Martin Fowler S

Slide 39

Slide 39 text

𝕏 @maxbbeckers Source: https://www.primevideotech.com/video-streaming/scaling-up-the-prime-video-audio-video-monitoring-service-and-reducing-costs-by-90

Slide 40

Slide 40 text

Contact: 𝕏 Thanks for your attention! @maxbbeckers [email protected] linkedin.com/in/maximilianbeckers