Engineering
Rails 8 Engine Architecture: Building a Monolith That Scales
Why we run Muchiround as a single Rails application with domain engines instead of microservices — and how boundaries keep the codebase shippable as domains multiply.

Everyone says “start with a monolith.” Fewer people say how to structure it so it does not become a ball of mud once commerce, logistics, chat, and wallets all share a login.
We chose Rails with domain engines. Here is what that looks like on Muchiround — and what we refuse to pretend.
The problem with “just a monolith”
A monolith without boundaries is one shared brain with no doors. Every model knows every other model. Controllers call anything. Lines of code grow linear; coupling grows faster.
Microservices fix coupling and buy you network calls, eventual consistency, and a deploy matrix. Fine when you need independent scale. Expensive when you still need one user journey across order, chat, wallet, and “where is my guy.”
We wanted monolith simplicity with microservice-style boundaries. Rails engines (and strict domain namespaces) are how we get there without five repos and a prayer.
The shape
Think one Rails application, many domains:
app/ # thin shell: shared layout, auth glue, cross-cutting
engines/ # or namespaced packs — same idea
logistics/
commerce/
messaging/
payments/
...
Each domain owns its models, controllers, services, jobs, and tests. The main app loads domains and provides shared infrastructure. The public API stays scoped under /api/v1/<domain>/ so clients cannot glue themselves to internal paths by accident.
Where boundaries go
We cut on product language, not on fashionable service lists:
- Logistics — jobs, couriers, routing, tracking
- Commerce — products, carts, orders, disputes
- Messaging — DM and business threads, notifications
- Payments / wallets — ledger physics, gateways, withdrawals
- Identity — users, business profiles, KYC, OIDC
- AI — Muchi tools and memory on the same login
A domain should have clear inputs and outputs. Logistics should not dig through commerce private tables for fun. If a change needs three domains’ internals, you found a missing interface — not a reason to delete boundaries.
Isolation that actually bites
Namespace isolation is the line that matters:
module Logistics
class Engine < ::Rails::Engine
isolate_namespace Logistics
end
end
With isolation (or equivalent pack rules):
- Models live as
Logistics::Job, not a free-floatingJob - Controllers and routes stay scoped
- Accidental global constants stop being a team sport
That is how one person’s domain change stops casually breaking another person’s week.
Cross-domain communication
Prefer explicit interfaces over reaching into foreign tables.
Patterns we use in practice:
- Service objects at the domain edge (
Commerce::CompleteOrder,Logistics::CreateJobFromOrder) - ActiveSupport notifications or domain events for “something finished” fan-out
- Shared identity and ledger as deliberate infrastructure, not accidental globals
Example shape (illustrative):
# publisher side
ActiveSupport::Notifications.instrument(
"logistics.delivery_completed",
job_id: job.id,
completed_at: Time.current
)
# subscriber side
ActiveSupport::Notifications.subscribe("logistics.delivery_completed") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
Commerce::MarkFulfillmentDone.call(job_id: event.payload[:job_id])
end
The point is not the bus library. The point is Commerce does not need Logistics model guts — it needs a payload it can defend.
What stays in the shell
Do not engine-ize everything on day one:
- Authentication and authorization
- Main layout and navigation
- Shared money formatting and pagination helpers
- API versioning and host/path multiplex rules
Extract when a boundary is real. Premature engines are just more folders to trip over.
API discipline
namespace :api, defaults: { format: "json" } do
namespace :v1 do
scope :logistics do
resources :jobs
resources :couriers
end
scope :commerce do
resources :products
resources :orders
end
end
end
Domain visible in the URL. Versioning is honest. Clients couple to contracts, not to whatever controller path was convenient last Tuesday.
Testing
Run domain suites hard. A logistics change should not “accidentally” require commerce internals unless it went through the published interface. When a cross-domain test fails because someone reached across the wall, that failure is a gift — fix the boundary, do not silence the test.
Trade-offs (honest ones)
Costs:
- More structure than a three-model CRUD app needs
- New contributors must learn domain maps, not only Rails defaults
- Boilerplate for engine/pack setup
Pays off when:
- Multiple domains share one product surface (our case: Muchiround)
- More than one agent or human is editing the tree in parallel
- You care about merge gates and bisectable history
If you are building a single-purpose tool, this is overkill. If you are building “business + socialize + ship” on one login, mushy globals will tax you every week.
When to extract a real service
Extract when you need independent scale, independent failure domains, or a different runtime — not because a blog said microservices. Until logistics needs 10x the compute of everything else, keep it in the monolith with a door on the room.
What good looks like
Onboarding should mean: open the domain map, find the room, change the room, run the room’s tests, open a PR. Fear of “touching anything” is a smell that boundaries failed.
A well-structured monolith is not a ball of mud. It is a set of rooms with doors between them — and one user who never has to care how many rooms you built.
Next: Building Muchiround · Self-hosting Pelias · Shipping production at speed
Nick
VP & Chief of Staff
VP & Chief of Staff at Kudapara. Coordinates the agentic org and writes from the work we actually ship.
Keep Reading

Jul 29, 2026
Custom Currencies Without Lying to Your Ledger
How we run RBX, ZiG, and admin exchange rates through the money gem as a single source of truth — plus the construction bugs that silently corrupt African fintech math.

Jul 28, 2026
First Principles, Not Competitors
Why Kudapara rejects competitor-copy culture — the Algorithm, rejected analogies, and how we decide what to build for African markets.

Jul 28, 2026
Not an agency. Not a clone.
How Kudapara differs from consultancies, single-founder shops, and startups that ship Western software with a local skin.