BuildWithMatija
Get In Touch
  1. Home
  2. Blog
  3. Medusa.js
  4. Medusa Architecture Explained: How It Works for Developers

Medusa Architecture Explained: How It Works for Developers

Developer breakdown of Medusa's modules, services, workflows, links, and Admin extensions for building custom commerce

20th April 2026·Updated on:1st April 2026·MŽMatija Žiberna·
Medusa.js
Early Access

You are viewing this article before its public release.

This goes live on April 20, 2026 at 6:00 AM.

Medusa Architecture Explained: How It Works for Developers

📚 Get Practical Development Guides

Join developers getting comprehensive guides, code examples, optimization tips, and time-saving prompts to accelerate their development workflow.

No spam. Unsubscribe anytime.

The first time I opened a Medusa codebase, it felt almost too empty to be serious.

There was no giant app layer full of controllers and business logic. Mostly I saw src/modules, src/workflows, src/api, and src/admin, plus a lot of commerce behavior that clearly existed somewhere but not inside the project tree I was editing. The important shift was realizing that a Medusa app is not supposed to contain the whole commerce engine. It is supposed to contain the part you are changing. Medusa ships a lot of the core domain behavior already, and your code sits around it as an extension layer. That part is straight from the architecture docs, but it only really clicked once I tried to add a feature that crossed carts, orders, stock locations, admin UI, and checkout hooks. (Medusa Docs)

The feature I used to understand that architecture was a pickup scheduling prototype. I wanted admins to define pickup slots against existing stock locations, let shoppers choose one on the cart, and then carry that selection onto the final order after checkout. That sounds small. In practice it forced me to touch almost every Medusa extension seam that matters: a custom module, module links, workflows, custom API routes, an Admin route, migration generation, and a checkout hook.

That concrete build changed how I think about Medusa. Before that, the architecture sounded elegant in the abstract. After building against it, it felt more like this:

Medusa's codebase feels empty at first, and that is both its strength and its tax.

The strength is obvious once you accept the model. You are not fighting a giant monolith to add one business-specific feature. The tax is that you need to learn where Medusa expects each kind of logic to live, and until that mental map settles, even a modest feature can feel more ceremonious than it would in a conventional ORM-first app.

The pickup scheduling build that made the architecture click

Here is the shape of the feature I built:

  • Reused Medusa stock_location records as pickup locations instead of inventing a second location model.
  • Added a custom module with two records: pickup_slot and pickup_selection.
  • Linked pickup_slot to stock_location, and pickup_selection to cart and order.
  • Exposed the feature through custom admin and store routes.
  • Moved the selected slot from cart scope to order scope when checkout completed.

That single feature ended up being the clearest explanation of Medusa's architecture I could find, because it made each concept show up under pressure instead of as a glossary item.

Why the codebase feels thin

Medusa projects look thin because most of the core commerce behavior is already packaged into built-in modules. Your local code is mostly the customization layer, not the whole system. That means the repo can look underwhelming on day one and surprisingly capable on day ten.

I think this is one of Medusa's best traits if you are building commerce features that do not justify forking the platform. But it also means onboarding is weirder than in systems where the entire domain model is visible in one app tree. With Payload, for example, a lot of meaning is concentrated in the collection definition. With Medusa, meaning is distributed across modules, workflows, routes, links, and hooks. Once you stop looking for one central model layer, the project layout makes more sense.

Module, service, workflow, route: what those layers feel like in practice

On paper, these layers are easy to define. In practice, they feel more opinionated than many Node backends.

A module is where new business data really belongs. For pickup scheduling, that meant defining pickup_slot and pickup_selection inside one isolated feature boundary instead of scattering that logic across carts or orders. Medusa documents modules as reusable domain packages, and that framing is accurate. The part the docs do not really convey is that a module makes the codebase feel calmer once the feature is working, because it gives you one obvious home for the custom domain instead of making you thread custom fields through core entities. (Medusa Docs)

A service is the runtime interface for that module. That part is conventional enough. The service owns CRUD-like behavior and feature-specific operations against the models. (Medusa Docs)

A workflow is where Medusa becomes distinctly Medusa. I used workflows for slot creation, slot deletion, selection changes on the cart, and final confirmation on order creation. That separation felt heavy at first, especially for simple mutations. After the feature started spanning validation, capacity changes, reselection logic, and order finalization, the workflow boundary stopped feeling like ceremony and started feeling like guardrails. It gave the feature a place for business process logic that was broader than one service call but narrower than "misc backend glue."

A route is deliberately thin. In this build, the routes mostly validated payloads, resolved dependencies, and handed control to workflows. That split is one of the more convincing parts of Medusa's architecture once you have lived with it for a bit. It discourages the slow drift where route handlers become your real application.

Links are elegant once they click, but they are not free

The most distinctive part of this build was Medusa's link model.

I linked:

  • pickup_slot to stock_location
  • pickup_selection to cart
  • pickup_selection to order

That let me keep the pickup scheduling data in its own module instead of modifying Medusa core tables. Architecturally, I think this is the right idea. Cross-module relationships stay explicit, and module isolation remains real instead of being something people talk about and then immediately violate.

But links have a learning cost. If you come from a typical ORM mindset, they feel heavier than a normal relation at first. You are not just saying "these records relate." You are accepting Medusa's rule that cross-domain connections need their own explicit mechanism. Once I accepted that, the model felt clean. Before that, it felt like extra ceremony for something an ORM would let me express in one line. Both reactions are true.

For this pickup feature, I deliberately kept both the raw IDs and the Medusa links:

  • The plain IDs made filtering and MVP business logic simpler.
  • The links kept the feature aligned with Medusa's extension model.

That hybrid approach felt pragmatic. Purists may prefer pushing everything through the full Medusa link/query path, but for a feature prototype I found it useful to keep the logic readable without throwing away the architecture.

Stock locations were the right reuse point

One decision that paid off immediately was treating Medusa stock locations as pickup locations instead of inventing a parallel location system.

That reuse gave me admin-managed places for free and kept the feature aligned with Medusa's existing domain model. It also exposed one of the more interesting Medusa tradeoffs: when the platform already has a concept that is close to what you need, the best custom feature is often not a brand new model. It is a thin extension around the existing concept.

That sounds obvious, but it changes the way you design features in Medusa. You spend less time asking "what tables should I add?" and more time asking "which built-in domain should this feature attach itself to?"

The checkout hook is where the architecture stopped being theoretical

The part that made the whole system feel real was the order-created hook.

When checkout completed, I used createOrderWorkflow.hooks.orderCreated(...) to run a custom workflow that:

  • found the cart's pickup selection
  • moved it from cart scope to order scope
  • updated the selection status from selected to confirmed
  • removed the cart link
  • created the order link

That small handoff made the architecture click harder than any docs page did. Medusa is not just asking you to store extra data. It wants you to model business state transitions as explicit flows. Once I had one real transition working across cart and order domains, the rest of the architecture made much more sense.

The Admin extension model is practical, but slightly uneven

I added lightweight Admin routes so an admin could browse pickup-capable locations, view slots for a location, create slots, and delete them.

This worked well, but it also surfaced a few practical rough edges that are worth naming because they are the kind of thing docs rarely emphasize:

  • The admin pages had to load locations from /admin/stock-locations, not from the storefront pickup route, because the store routes expect a publishable API key and the Admin already has its own session model.
  • Using ordinary /app/... links was safer than assuming custom Admin routes would expose router context exactly like a normal dashboard tree.
  • A lightweight route-based prototype was much faster to ship than trying to jump directly into a more elaborate widget architecture.

None of those issues are deal breakers. They are just the kind of implementation detail that tells you whether someone actually tried to build on the system.

Migrations and generated types are still real work

Another useful correction to the "thin codebase" impression is that Medusa does not remove the hard parts of application development. It relocates them.

My custom models still needed migrations because they become real database tables. Link definitions also imply real database structures. Medusa's data model and module-link docs cover that part accurately. (Medusa Docs, Medusa Docs)

The same is true for typing. Medusa generates useful framework types under .medusa, but it does not generate every business input shape you need. I still had to define feature-level command inputs like slot creation payloads and pickup selection inputs by hand, because those types express application contracts, not just model shapes. (Medusa Docs)

That is not a flaw. It is just worth stating plainly. Medusa reduces a lot of plumbing, but it does not eliminate domain modeling.

What I think Medusa is good at after this build

After building pickup scheduling on top of Medusa, I think the framework is strongest when:

  • you want to add a business-specific commerce capability without rewriting core commerce domains
  • you are comfortable learning a more explicit architecture up front in exchange for cleaner feature boundaries later
  • you want extension points for backend logic, Admin surfaces, and stateful business flows to all follow one system

The thing I would warn new developers about is that Medusa does not feel "simple" on first contact. It feels sparse, then abstract, then eventually opinionated in a useful way. If you expect a normal app skeleton where the model layer reveals everything immediately, you may bounce off it. If you accept that the project is mostly an extension shell around a larger commerce engine, it starts to read much more clearly.

The real mental model

The best short version I can give after building a real feature is this:

Medusa is not an ecommerce app you gradually fill in.

It is a commerce framework that expects your business logic to live in explicit extension points:

  • modules for new domains
  • links for cross-domain associations
  • workflows for business processes
  • routes for thin HTTP surfaces
  • Admin extensions for merchant tooling

That structure is why the codebase feels empty at first. It is also why a feature like pickup scheduling can cross stock locations, carts, orders, checkout, and Admin UI without requiring a fork of Medusa core.

The docs are useful for understanding the official model. The practical lesson from building on it is that Medusa works best once you stop asking "where is the whole app?" and start asking "which extension seam owns this piece of business logic?"

📄View markdown version
10

Frequently Asked Questions

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

Table of Contents

  • The pickup scheduling build that made the architecture click
  • Why the codebase feels thin
  • Module, service, workflow, route: what those layers feel like in practice
  • Links are elegant once they click, but they are not free
  • Stock locations were the right reuse point
  • The checkout hook is where the architecture stopped being theoretical
  • The Admin extension model is practical, but slightly uneven
  • Migrations and generated types are still real work
  • What I think Medusa is good at after this build
  • The real mental model
On this page:
  • The pickup scheduling build that made the architecture click
  • Why the codebase feels thin
  • Module, service, workflow, route: what those layers feel like in practice
  • Links are elegant once they click, but they are not free
  • Stock locations were the right reuse point
Build With Matija Logo

Build with Matija

Matija Žiberna

I turn scattered business knowledge into one usable system. End-to-end system architecture, AI integration, and development.

Quick Links

Case Studies
  • Other Projects
  • How I Work
  • Blog
  • RSS Feed
  • Services

    • B2B Website Development
    • Bespoke AI Applications
    • Advisory

    Payload

    • B2B Website Development
    • Payload CMS Developer
    • Audit
    • Migration
    • Pricing
    • Payload vs Sanity
    • Payload vs WordPress
    • Payload vs Strapi
    • Payload vs Contentful

    Industries

    • Manufacturing
    • Construction

    Get in Touch

    Have a project in mind? Let's discuss how we can help your business grow.

    Book a discovery callContact me →
    © 2026BuildWithMatija•Principal-led system architecture•All rights reserved

    Comments

    Leave a Comment

    Your email will not be published

    Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

    10-2000 characters

    • Comments are automatically approved and will appear immediately

    • Your name and email will be saved for future comments

    • Be respectful and constructive in your feedback

    • No spam, self-promotion, or off-topic content

    No comments yet

    Be the first to share your thoughts on this post!