Skip to main content

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:
1

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
2

Database Layer

PostgreSQL integration via MikroORM:
  • Automatic migrations
  • Entity relationships
  • Query builder
  • Transaction management
From @medusajs/framework/src/database/
3

HTTP Server

Express-based server with middleware:
  • CORS configuration
  • Authentication
  • Request validation
  • Response serialization
From @medusajs/framework/src/http/
4

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" })
5

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:
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:
1

HTTP Request

Client sends request to /admin/products
2

Routing

Express routes to src/api/admin/products/route.ts
3

Middleware

  • Authentication (JWT verification)
  • CORS headers
  • Request validation
  • Query parsing
4

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)
}
5

Workflow Invocation

Handler invokes workflow:
const { result } = await createProductsWorkflow(req.scope)
  .run({ input: req.body })
6

Step Execution

Workflow executes steps sequentially:
  • Validates input
  • Calls module services
  • Handles errors with compensation
7

Module Service

Module service performs business logic:
  • Data validation
  • Database operations
  • Event emission
8

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

Performance Considerations

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

Build docs developers (and LLMs) love