Architecture
Medusa is built on a modular, composable architecture that separates concerns into distinct layers: modules, workflows, API routes, and the framework runtime.
Architecture Overview
┌─────────────────────────────────────────────────────────┐
│ API Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Admin Routes │ │ Store Routes │ │Custom Routes │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ Workflow Layer │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Workflows (Business Logic Orchestration) │ │
│ │ - createProductWorkflow │ │
│ │ - processOrderWorkflow │ │
│ │ - Custom workflows │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ Module Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Product │ │ Cart │ │ Order │ │ Payment │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Customer │ │Inventory │ │Promotion │ │ ... │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ Framework Layer │
│ - Dependency Injection (Awilix) │
│ - Database (PostgreSQL via MikroORM) │
│ - HTTP Server (Express) │
│ - Event Bus │
│ - Caching │
│ - Job Queue │
└─────────────────────────────────────────────────────────┘
Core Concepts
Modules
Modules are self-contained packages that handle specific business domains. Each module:
Manages its own data models and database schema
Exposes a service interface for business logic
Can be used independently or composed together
Is versioned and published to npm
Module structure :
@ medusajs / product /
├── src /
│ ├── models / # Database entities
│ ├── services / # Business logic
│ ├── repositories / # Data access
│ ├── migrations / # Database migrations
│ └── index . ts # Module exports
Available modules :
Product Catalog, variants, options, collections
Cart Shopping cart, line items, totals
Order Order management, fulfillment, returns
Payment Payment processing, refunds
Customer Customer accounts, groups
Inventory Stock levels, reservations
Fulfillment Shipping, delivery providers
Promotion Discounts, campaigns, rules
Notification Multi-channel notifications
Workflows
Workflows orchestrate complex business processes by composing steps. They provide:
Compensation logic : Automatic rollback on failures
Type safety : Full TypeScript support
Composability : Reuse steps across workflows
Observability : Built-in hooks and events
Workflow anatomy :
import {
createWorkflow ,
createStep ,
StepResponse ,
WorkflowData ,
WorkflowResponse ,
} from "@medusajs/framework/workflows-sdk"
// Define a step with compensation
const reserveInventoryStep = createStep (
"reserve-inventory" ,
async ( productId : string , { container }) => {
const inventoryService = container . resolve ( "inventoryService" )
const reservation = await inventoryService . reserve ( productId , 1 )
// Return data and compensation data
return new StepResponse ( reservation , reservation . id )
},
// Compensation function (rollback)
async ( reservationId , { container }) => {
if ( ! reservationId ) return
const inventoryService = container . resolve ( "inventoryService" )
await inventoryService . cancelReservation ( reservationId )
}
)
// Compose workflow from steps
const createOrderWorkflow = createWorkflow (
"create-order" ,
( input : WorkflowData <{ productId : string ; customerId : string }>) => {
// Steps execute in sequence
const reservation = reserveInventoryStep ( input . productId )
const order = createOrderStep ({
customerId: input . customerId ,
productId: input . productId ,
})
const payment = processPaymentStep ( order . id )
return new WorkflowResponse ( order )
}
)
Workflow execution :
import { createOrderWorkflow } from "./workflows/create-order"
const { result , errors } = await createOrderWorkflow ( container )
. run ({
input: {
productId: "prod_123" ,
customerId: "cust_456" ,
},
})
if ( errors ) {
// All steps are automatically compensated
console . error ( "Order creation failed:" , errors )
} else {
console . log ( "Order created:" , result )
}
API Routes
API routes define HTTP endpoints with full framework integration:
// src/api/store/products/route.ts
import {
MedusaRequest ,
MedusaResponse ,
} from "@medusajs/framework/http"
import { HttpTypes } from "@medusajs/framework/types"
export const GET = async (
req : MedusaRequest < HttpTypes . StoreProductListParams >,
res : MedusaResponse < HttpTypes . StoreProductListResponse >
) => {
// Access query via dependency injection
const query = req . scope . resolve ( "query" )
const { data : products , metadata } = await query . graph ({
entity: "product" ,
fields: req . queryConfig . fields ,
filters: req . filterableFields ,
pagination: req . queryConfig . pagination ,
})
res . json ({
products ,
count: metadata . count ,
offset: metadata . skip ,
limit: metadata . take ,
})
}
export const POST = async (
req : MedusaRequest < HttpTypes . AdminCreateProduct >,
res : MedusaResponse < HttpTypes . AdminProductResponse >
) => {
const { result } = await createProductsWorkflow ( req . scope )
. run ({
input: { products: [ req . body ] },
})
res . json ({ product: result [ 0 ] })
}
Route file location determines URL :
src/api/
├── admin/
│ └── products/
│ ├── route.ts → /admin/products
│ └── [id]/
│ └── route.ts → /admin/products/:id
└── store/
└── products/
└── route.ts → /store/products
Framework Runtime
The @medusajs/framework package provides the core runtime that powers Medusa:
Key components :
Dependency Injection Container
Uses Awilix for dependency management: // Access any service or module
const productService = container . resolve ( "productModuleService" )
const query = container . resolve ( "query" )
From @medusajs/framework/src/container.ts
Database Layer
PostgreSQL integration via MikroORM :
Automatic migrations
Entity relationships
Query builder
Transaction management
From @medusajs/framework/src/database/
HTTP Server
Express-based server with middleware:
CORS configuration
Authentication
Request validation
Response serialization
From @medusajs/framework/src/http/
Event Bus
Pub/sub system for async communication: import { Modules } from "@medusajs/framework/utils"
const eventBus = container . resolve ( Modules . EVENT_BUS )
await eventBus . emit ( "order.placed" , { orderId: "123" })
Workflow Engine
Orchestration runtime from @medusajs/orchestration:
Step execution
Compensation handling
State management
Retry logic
Module Architecture
Each module follows a consistent internal structure:
Service Layer
Services contain business logic and use decorators for cross-cutting concerns:
// packages/modules/product/src/services/product-module-service.ts
import {
InjectManager ,
InjectTransactionManager ,
MedusaContext ,
MedusaService ,
} from "@medusajs/framework/utils"
export class ProductModuleService
extends MedusaService <{ Product : { dto : ProductDTO } }>
implements IProductModuleService
{
// Public method with manager injection
@ InjectManager ()
async createProducts (
data : CreateProductDTO [],
@ MedusaContext () sharedContext : Context = {}
) {
return await this . createProducts_ ( data , sharedContext )
}
// Protected implementation with transaction manager
@ InjectTransactionManager ()
protected async createProducts_ (
data : CreateProductDTO [],
@ MedusaContext () sharedContext : Context = {}
) {
const products = data . map (( d ) => this . productService_ . create ( d ))
return await this . productService_ . save ( products , sharedContext )
}
}
Decorator patterns from CLAUDE.md :
@InjectManager() - Inject entity manager (public methods)
@InjectTransactionManager() - Inject transaction manager (protected methods)
@MedusaContext() - Inject shared context parameter
@EmitEvents() - Emit domain events after operation
Model Layer
Data models use MikroORM entities:
import { model } from "@medusajs/framework/utils"
const Product = model . define ( "product" , {
id: model . id (). primaryKey (),
title: model . text (),
description: model . text (). nullable (),
handle: model . text (),
status: model . enum ([ "draft" , "published" , "rejected" ]),
variants: model . hasMany (() => ProductVariant ),
categories: model . manyToMany (() => ProductCategory ),
created_at: model . dateTime (),
updated_at: model . dateTime (),
})
Repository Layer
Repositories handle data access:
export class ProductRepository extends DAL . Repository {
async findByHandle ( handle : string ) {
return await this . find ({ handle })
}
async findWithVariants ( id : string ) {
return await this . findOne (
{ id },
{ populate: [ "variants" ] }
)
}
}
Configuration
Medusa’s configuration is defined in medusa-config.ts:
import { defineConfig , Modules } from "@medusajs/framework/utils"
export default defineConfig ({
// Project configuration
projectConfig: {
databaseUrl: process . env . DATABASE_URL ,
http: {
storeCors: process . env . STORE_CORS ,
adminCors: process . env . ADMIN_CORS ,
jwtSecret: process . env . JWT_SECRET ,
cookieSecret: process . env . COOKIE_SECRET ,
},
redisUrl: process . env . REDIS_URL ,
workerMode: "shared" , // or "worker" or "server"
} ,
// Admin configuration
admin: {
path: "/app" ,
backendUrl: process . env . MEDUSA_BACKEND_URL ,
} ,
// Module configuration
modules: {
[Modules. PRODUCT ]: {
resolve: "@medusajs/product" ,
},
[Modules. PAYMENT ]: {
resolve: "@medusajs/payment" ,
options: {
providers: [
{
resolve: "@medusajs/payment-stripe" ,
id: "stripe" ,
options: {
apiKey: process . env . STRIPE_API_KEY ,
},
},
],
},
},
[Modules. FILE ]: {
resolve: "@medusajs/file" ,
options: {
providers: [
{
resolve: "@medusajs/file-s3" ,
id: "s3" ,
options: {
bucket: process . env . S3_BUCKET ,
region: process . env . S3_REGION ,
},
},
],
},
},
} ,
// Feature flags
featureFlags: {
index_engine: true ,
} ,
})
Configuration handling from packages/core/framework/src/config/config.ts
Monorepo Structure
Medusa’s source code is organized as a Yarn 3 monorepo:
medusa/
├── packages/
│ ├── core/
│ │ ├── framework/ # @medusajs/framework
│ │ ├── workflows-sdk/ # @medusajs/workflows-sdk
│ │ ├── core-flows/ # @medusajs/core-flows
│ │ ├── types/ # @medusajs/types
│ │ ├── utils/ # @medusajs/utils
│ │ └── modules-sdk/ # @medusajs/modules-sdk
│ ├── modules/
│ │ ├── product/ # @medusajs/product
│ │ ├── cart/ # @medusajs/cart
│ │ ├── order/ # @medusajs/order
│ │ ├── payment/ # @medusajs/payment
│ │ └── .../ # 30+ modules
│ ├── medusa/ # @medusajs/medusa (main package)
│ ├── admin/
│ │ └── dashboard/ # @medusajs/admin-dashboard
│ └── cli/
│ ├── medusa-cli/ # @medusajs/cli
│ └── create-medusa-app/ # create-medusa-app
├── integration-tests/ # Full-stack tests
└── package.json # Workspace config
From ~/workspace/source/README.md and package.json
Request Lifecycle
Understanding how a request flows through Medusa:
HTTP Request
Client sends request to /admin/products
Routing
Express routes to src/api/admin/products/route.ts
Middleware
Authentication (JWT verification)
CORS headers
Request validation
Query parsing
Handler Execution
Route handler executes: export const GET = async ( req , res ) => {
// Handler has access to:
// - req.scope (DI container)
// - req.filterableFields (parsed filters)
// - req.queryConfig (fields, pagination)
}
Workflow Invocation
Handler invokes workflow: const { result } = await createProductsWorkflow ( req . scope )
. run ({ input: req . body })
Step Execution
Workflow executes steps sequentially:
Validates input
Calls module services
Handles errors with compensation
Module Service
Module service performs business logic:
Data validation
Database operations
Event emission
Response
Handler returns JSON response: res . json ({ product: result })
Error Handling
Medusa uses MedusaError for consistent error handling:
import { MedusaError } from "@medusajs/framework/utils"
// Throw typed errors
if ( ! product ) {
throw new MedusaError (
MedusaError . Types . NOT_FOUND ,
`Product with id: ${ id } was not found`
)
}
if ( order . status === "cancelled" ) {
throw new MedusaError (
MedusaError . Types . NOT_ALLOWED ,
"Cannot update a cancelled order"
)
}
Error types :
NOT_FOUND - Resource doesn’t exist
INVALID_DATA - Invalid input or state
NOT_ALLOWED - Operation not permitted
DUPLICATE_ERROR - Unique constraint violation
UNAUTHORIZED - Authentication required
PAYMENT_AUTHORIZATION_ERROR - Payment processing failed
From CLAUDE.md section 5.4
Event System
Modules emit events for async operations:
// Subscribe to events
import { Modules } from "@medusajs/framework/utils"
export default async function productCreatedHandler ({
event ,
container ,
}) {
const { id } = event . data
const productService = container . resolve ( Modules . PRODUCT )
// Perform async operation
const product = await productService . retrieve ( id )
await sendNotification ( product )
}
export const config = {
event: "product.created" ,
}
Common events :
product.created, product.updated, product.deleted
order.placed, order.fulfilled, order.canceled
cart.created, cart.updated
customer.created, customer.updated
Development Workflow
Local Development
# Watch mode with hot reload
npm run dev
# Build TypeScript
npm run build
# Run tests
npm test
# Database migrations
npm run migrations:run
Testing Strategy
Unit tests : packages/*/__tests__/*.spec.ts
Integration tests : packages/*/integration-tests/__tests__/*.spec.ts
API tests : integration-tests/http/__tests__/*.spec.ts
Framework : Jest 29 (backend), Vitest 3 (frontend)
From CLAUDE.md section 3
Database Indexing : Modules automatically create indexes on foreign keys and frequently queried fields.
Query Optimization : Use the Query API for efficient data fetching with automatic join optimization.
Caching : Enable Redis for session storage and caching:projectConfig : {
redisUrl : process . env . REDIS_URL ,
}
Worker Mode : Separate HTTP and background workers:# Server mode (HTTP only)
MEDUSA_WORKER_MODE = server npm start
# Worker mode (background jobs only)
MEDUSA_WORKER_MODE = worker npm start
Next Steps
Create Your First Workflow Learn to build custom workflows with compensation logic
Build Custom Modules Extend Medusa with your own business domain modules
API Route Development Create custom API endpoints with full type safety
Module Development Deep dive into module architecture and patterns