← Back to list

Why Shopify Rejected Microservices (And What They Did Instead)

The $100B e-commerce platform runs on a monolith. Here’s why that wasn’t a mistake — and how you can apply their lessons.

Mori in StartupInsider · 2026-06-28 17:01 · 1 claps · 11.5 min read paywalled
#software-development #software-engineering #programming #dotnet #csharp
Open on Medium ↗
Wiki topics: 💻 · Programming

Why Shopify Rejected Microservices (And What They Did Instead)

The $100B e-commerce platform runs on a monolith. Here’s why that wasn’t a mistake — and how you can apply their lessons.

Every conference talk in 2016–2020 pushed the same narrative: “Monoliths are legacy. Microservices are the future. If you’re not breaking apart your app, you’re already behind.”

Shopify heard that message. They were a Ruby on Rails monolith handling $40B+ in GMV. Engineers were leaving because “Rails doesn’t scale.” Recruiters at other companies used their architecture as a negative selling point. The pressure to microservice was immense.

So Shopify did what every blog post recommended. They extracted their first service: the shipping rate calculator. It took 6 months. Deployment complexity tripled. A simple tax change now required coordinated deploys across three services. Debugging a checkout flow meant querying five different logs. The team that built it eventually admitted: “We made the wrong call.”

They reverted. Not back to a messy monolith, but forward to something better: a modular monolith with component-based architecture. Today, Shopify processes peak loads of 3M+ requests per minute during Black Friday on that architecture. Their engineering team has grown to 10,000+ developers. And they’re still not running microservices.

This article breaks down Shopify’s actual architecture, why it works, and how to apply their principles in .NET 10.

The Microservices Trap Shopify Avoided

What Shopify Actually Tried

In 2016, Shopify extracted their shipping rate calculation into a standalone service. The logic seemed clean: shipping is complex, involves third-party APIs (FedEx, UPS, DHL), and changes frequently.

What happened:

The shipping service added operational overhead without proportional value. As Shopify’s VP of Engineering, Farhan Thawar, later noted: “The service boundary was wrong. We extracted by technical function, not business capability. The shipping calculator needs deep access to order data, cart contents, and customer addresses — data that lives in the monolith. The service boundary created more coupling, not less.”

The Realization

Shopify’s leadership asked a critical question: “Is our problem the monolith, or is our problem monolith management?”

The answer was the latter. Their issues weren’t architectural — they were organizational:

  • No clear module ownership
  • No enforced boundaries between domains
  • No incremental deployment of monolith components
  • Database tables accessed by every team

Microservices don’t solve organizational problems. They just make them harder to fix.

What Shopify Built Instead: The Modular Monolith

Shopify’s current architecture (as described in their engineering blog and conference talks) has three layers:

┌─────────────────────────────────────────────────────────────────────┐
│                         Shopify Monolith                             │
│                    (Single Deployable Unit)                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    Component Layer                             │   │
│  │  ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐  │   │
│  │  │  Checkout    │ │  Payments    │ │  Fulfillment        │  │   │
│  │  │  Component   │ │  Component   │ │  Component          │  │   │
│  │  │              │ │              │ │                     │  │   │
│  │  │ • Cart logic │ │ • Stripe    │ │ • Inventory check   │  │   │
│  │  │ • Discounts  │ │ • PayPal    │ │ • Warehouse routing │  │   │
│  │  │ • Taxes      │ │ • Shopify   │ │ • Shipping rates    │  │   │
│  │  │ • Shipping   │ │   Payments  │ │ • Tracking          │  │   │
│  │  └──────┬───────┘ └──────┬──────┘ └──────────┬──────────┘  │   │
│  │         │                │                    │             │   │
│  │         └────────────────┼────────────────────┘             │   │
│  │                          │                                   │   │
│  │              ┌───────────┴───────────┐                       │   │
│  │              │    Core Platform       │                       │   │
│  │              │  (Shared infrastructure) │                       │   │
│  │              └─────────────────────────┘                       │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    Data Layer                                    │   │
│  │  ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐  │   │
│  │  │  Orders DB   │ │  Products DB │ │  Analytics DB       │  │   │
│  │  │  (Primary)   │ │  (Read replicas)│ │  (ClickHouse)      │  │   │
│  │  └─────────────┘ └─────────────┘ └─────────────────────┘  │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │              Background Job Layer (Sidekiq)                    │   │
│  │  • Email sends • Webhook delivery • Report generation         │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

Key principles:

  1. Components, not services. Components live in the same codebase, same deployable, but have enforced boundaries.
  2. Database per component. No shared tables. Components access other components’ data only through public APIs.
  3. Independent deployment of components. Shopify uses a custom deployment system that can deploy individual components without full monolith redeploy.
  4. Event-driven communication. Components communicate via Kafka events, not direct database access.

Applying Shopify’s Architecture in .NET 10

Let’s build a Shopify-inspired modular monolith for an e-commerce platform.

Project Structure

EcommerceModular/
├── src/
│   ├── Core/
│   │   ├── SharedKernel/           # Domain primitives, events
│   │   └── Infrastructure/         # Cross-cutting concerns
│   ├── Components/
│   │   ├── Catalog.Component/     # Products, categories, search
│   │   ├── Pricing.Component/      # Discounts, promotions, taxes
│   │   ├── Inventory.Component/    # Stock, warehouses, reservations
│   │   ├── Checkout.Component/       # Cart, shipping calculation
│   │   ├── Payments.Component/       # Payment processing, refunds
│   │   └── Notifications.Component/  # Email, SMS, webhooks
│   └── Ecommerce.API/             # Host application
└── tests/
    ├── ComponentTests/
    └── IntegrationTests/

The Shared Kernel

src/Core/SharedKernel/Domain/Entity.cs:

namespace SharedKernel.Domain;
public abstract class Entity
{
    public Guid Id { get; protected set; }
    public DateTime CreatedAt { get; protected set; }
    public DateTime? UpdatedAt { get; protected set; }
    private readonly List<DomainEvent> _domainEvents = new();
    public IReadOnlyCollection<DomainEvent> DomainEvents => _domainEvents.AsReadOnly();
    protected Entity() => Id = Guid.NewGuid();
    public void AddDomainEvent(DomainEvent @event) => _domainEvents.Add(@event);
    public void ClearDomainEvents() => _domainEvents.Clear();
}
public abstract class DomainEvent
{
    public Guid Id { get; } = Guid.NewGuid();
    public DateTime OccurredAt { get; } = DateTime.UtcNow;
    public string EventType => GetType().FullName!;
}

src/Core/SharedKernel/Events/IEventBus.cs:

namespace SharedKernel.Events;
public interface IEventBus
{
    Task PublishAsync<T>(T @event, CancellationToken ct = default) where T : DomainEvent;
    Task SubscribeAsync<T>(Func<T, CancellationToken, Task> handler, CancellationToken ct = default) where T : DomainEvent;
}

Component Contract — The Public API

Components expose only contracts. Other components cannot see internals.

src/Components/Catalog.Component/Contracts/ICatalogQueries.cs:

namespace Catalog.Component.Contracts;
public record ProductSummary(Guid Id, string Name, string Sku, decimal Price, string Currency, bool IsAvailable);
public interface ICatalogQueries
{
    Task<ProductSummary?> GetProductAsync(Guid productId, CancellationToken ct = default);
    Task<IReadOnlyList<ProductSummary>> GetProductsAsync(IEnumerable<Guid> productIds, CancellationToken ct = default);
    Task<bool> IsAvailableAsync(Guid productId, int quantity, CancellationToken ct = default);
}

src/Components/Catalog.Component/Contracts/Events/ProductEvents.cs:

using SharedKernel.Domain;
namespace Catalog.Component.Contracts.Events;
public class ProductPriceChanged : DomainEvent
{
    public Guid ProductId { get; }
    public decimal OldPrice { get; }
    public decimal NewPrice { get; }
    public string Currency { get; }
    public ProductPriceChanged(Guid productId, decimal oldPrice, decimal newPrice, string currency)
    {
        ProductId = productId;
        OldPrice = oldPrice;
        NewPrice = newPrice;
        Currency = currency;
    }
}
public class ProductStockChanged : DomainEvent
{
    public Guid ProductId { get; }
    public int NewQuantity { get; }
    public ProductStockChanged(Guid productId, int newQuantity)
    {
        ProductId = productId;
        NewQuantity = newQuantity;
    }
}

Catalog Component — Internal Implementation

src/Components/Catalog.Component/Domain/Product.cs:

using Catalog.Component.Contracts.Events;
using SharedKernel.Domain;
namespace Catalog.Component.Domain;
public class Product : Entity
{
    public string Name { get; private set; } = default!;
    public string Sku { get; private set; } = default!;
    public decimal Price { get; private set; }
    public string Currency { get; private set; } = default!;
    public int StockQuantity { get; private set; }
    public bool IsActive { get; private set; }
    private Product() { }
    public Product(string name, string sku, decimal price, string currency, int initialStock)
    {
        Name = name;
        Sku = sku;
        Price = price;
        Currency = currency;
        StockQuantity = initialStock;
        IsActive = true;
    }
    public void UpdatePrice(decimal newPrice)
    {
        if (newPrice <= 0) throw new ArgumentException("Price must be positive");

        var oldPrice = Price;
        Price = newPrice;
        UpdatedAt = DateTime.UtcNow;
        AddDomainEvent(new ProductPriceChanged(Id, oldPrice, newPrice, Currency));
    }
    public void AdjustStock(int delta)
    {
        var newStock = StockQuantity + delta;
        if (newStock < 0) throw new InvalidOperationException("Insufficient stock");

        StockQuantity = newStock;
        UpdatedAt = DateTime.UtcNow;
        AddDomainEvent(new ProductStockChanged(Id, newStock));
    }
    public void Deactivate()
    {
        IsActive = false;
        UpdatedAt = DateTime.UtcNow;
    }
}

src/Components/Catalog.Component/Persistence/CatalogDbContext.cs:

using Catalog.Component.Domain;
using Microsoft.EntityFrameworkCore;
namespace Catalog.Component.Persistence;
public class CatalogDbContext : DbContext
{
    public CatalogDbContext(DbContextOptions<CatalogDbContext> options) : base(options) { }
    public DbSet<Product> Products => Set<Product>();
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema("catalog");
        modelBuilder.Entity<Product>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
            entity.Property(e => e.Sku).IsRequired().HasMaxLength(50);
            entity.HasIndex(e => e.Sku).IsUnique();
            entity.Property(e => e.Price).HasPrecision(18, 2);
            entity.Property(e => e.Currency).HasMaxLength(3);
        });
    }
}

src/Components/Catalog.Component/Services/CatalogQueries.cs:

using Catalog.Component.Contracts;
using Catalog.Component.Persistence;
using Microsoft.EntityFrameworkCore;
namespace Catalog.Component.Services;
public class CatalogQueries : ICatalogQueries
{
    private readonly CatalogDbContext _dbContext;
    public CatalogQueries(CatalogDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    public async Task<ProductSummary?> GetProductAsync(Guid productId, CancellationToken ct = default)
    {
        var product = await _dbContext.Products
            .AsNoTracking()
            .FirstOrDefaultAsync(p => p.Id == productId, ct);
        return product is null ? null : MapToSummary(product);
    }
    public async Task<IReadOnlyList<ProductSummary>> GetProductsAsync(
        IEnumerable<Guid> productIds, 
        CancellationToken ct = default)
    {
        var ids = productIds.ToList();
        var products = await _dbContext.Products
            .AsNoTracking()
            .Where(p => ids.Contains(p.Id))
            .ToListAsync(ct);
        return products.Select(MapToSummary).ToList();
    }
    public async Task<bool> IsAvailableAsync(Guid productId, int quantity, CancellationToken ct = default)
    {
        var product = await _dbContext.Products
            .AsNoTracking()
            .FirstOrDefaultAsync(p => p.Id == productId, ct);
        return product?.IsActive == true && product.StockQuantity >= quantity;
    }
    private static ProductSummary MapToSummary(Domain.Product p) => new(
        p.Id, p.Name, p.Sku, p.Price, p.Currency, p.IsActive && p.StockQuantity > 0);
}

Pricing Component — Reacting to Catalog Events

src/Components/Pricing.Component/Domain/Discount.cs:

using SharedKernel.Domain;
namespace Pricing.Component.Domain;
public class Discount : Entity
{
    public string Code { get; private set; } = default!;
    public decimal Percentage { get; private set; }
    public DateTime ValidFrom { get; private set; }
    public DateTime ValidTo { get; private set; }
    public List<Guid> ApplicableProductIds { get; private set; } = new();
    public bool IsActive { get; private set; }
    private Discount() { }
    public Discount(string code, decimal percentage, DateTime validFrom, DateTime validTo)
    {
        Code = code;
        Percentage = percentage;
        ValidFrom = validFrom;
        ValidTo = validTo;
        IsActive = true;
    }
    public bool IsValidFor(Guid productId, DateTime date)
    {
        return IsActive 
            && date >= ValidFrom 
            && date <= ValidTo 
            && (ApplicableProductIds.Count == 0 || ApplicableProductIds.Contains(productId));
    }
    public decimal ApplyDiscount(decimal originalPrice)
    {
        return originalPrice * (1 - Percentage / 100);
    }
}

src/Components/Pricing.Component/EventHandlers/ProductPriceChangedHandler.cs:

using Catalog.Component.Contracts.Events;
using Microsoft.Extensions.Logging;
using Pricing.Component.Persistence;
using SharedKernel.Events;
namespace Pricing.Component.EventHandlers;
public class ProductPriceChangedHandler
{
    private readonly PricingDbContext _dbContext;
    private readonly ILogger<ProductPriceChangedHandler> _logger;
    public ProductPriceChangedHandler(PricingDbContext dbContext, ILogger<ProductPriceChangedHandler> logger)
    {
        _dbContext = dbContext;
        _logger = logger;
    }
    public async Task HandleAsync(ProductPriceChanged @event, CancellationToken ct)
    {
        _logger.LogInformation(
            "Product {ProductId} price changed from {OldPrice} to {NewPrice}. Re-evaluating active discounts.",
            @event.ProductId,
            @event.OldPrice,
            @event.NewPrice);
        // Re-evaluate if any discounts need adjustment based on new price
        var affectedDiscounts = await _dbContext.Discounts
            .Where(d => d.IsActive && d.ApplicableProductIds.Contains(@event.ProductId))
            .ToListAsync(ct);
        foreach (var discount in affectedDiscounts)
        {
            var newDiscountedPrice = discount.ApplyDiscount(@event.NewPrice);
            _logger.LogInformation(
                "Discount {DiscountCode} now yields price {NewPrice} for product {ProductId}",
                discount.Code,
                newDiscountedPrice,
                @event.ProductId);
        }
        // In real Shopify: trigger cache invalidation, update search index, notify merchants
    }
}

Checkout Component — Orchestrating Multiple Components

src/Components/Checkout.Component/Domain/Cart.cs:

using SharedKernel.Domain;
namespace Checkout.Component.Domain;
public class Cart : Entity
{
    public Guid CustomerId { get; private set; }
    private readonly List<CartItem> _items = new();
    public IReadOnlyCollection<CartItem> Items => _items.AsReadOnly();
    private Cart() { }
    public Cart(Guid customerId)
    {
        CustomerId = customerId;
    }
    public void AddItem(Guid productId, string productName, int quantity, decimal unitPrice, string currency)
    {
        var existing = _items.FirstOrDefault(i => i.ProductId == productId);
        if (existing != null)
        {
            existing.IncreaseQuantity(quantity);
        }
        else
        {
            _items.Add(new CartItem(productId, productName, quantity, unitPrice, currency));
        }
        UpdatedAt = DateTime.UtcNow;
    }
    public void RemoveItem(Guid productId)
    {
        _items.RemoveAll(i => i.ProductId == productId);
        UpdatedAt = DateTime.UtcNow;
    }
    public Money CalculateSubtotal()
    {
        if (!_items.Any()) return new Money(0, "USD");
        var currency = _items.First().Currency;
        var total = _items.Sum(i => i.UnitPrice * i.Quantity);
        return new Money(total, currency);
    }
    public bool CanCheckout()
    {
        return _items.Any() && _items.All(i => i.Quantity > 0);
    }
}
public class CartItem
{
    public Guid ProductId { get; private set; }
    public string ProductName { get; private set; }
    public int Quantity { get; private set; }
    public decimal UnitPrice { get; private set; }
    public string Currency { get; private set; }
    private CartItem() { }
    public CartItem(Guid productId, string productName, int quantity, decimal unitPrice, string currency)
    {
        ProductId = productId;
        ProductName = productName;
        Quantity = quantity;
        UnitPrice = unitPrice;
        Currency = currency;
    }
    public void IncreaseQuantity(int amount) => Quantity += amount;
}

src/Components/Checkout.Component/Services/CheckoutService.cs:

using Catalog.Component.Contracts;
using Checkout.Component.Domain;
using Checkout.Component.Persistence;
using Inventory.Component.Contracts;
using Microsoft.Extensions.Logging;
using Pricing.Component.Contracts;
using SharedKernel.Domain;
namespace Checkout.Component.Services;
public class CheckoutService
{
    private readonly CheckoutDbContext _dbContext;
    private readonly ICatalogQueries _catalogQueries;
    private readonly IPricingQueries _pricingQueries;
    private readonly IInventoryQueries _inventoryQueries;
    private readonly ILogger<CheckoutService> _logger;
    public CheckoutService(
        CheckoutDbContext dbContext,
        ICatalogQueries catalogQueries,
        IPricingQueries pricingQueries,
        IInventoryQueries inventoryQueries,
        ILogger<CheckoutService> logger)
    {
        _dbContext = dbContext;
        _catalogQueries = catalogQueries;
        _pricingQueries = pricingQueries;
        _inventoryQueries = inventoryQueries;
        _logger = logger;
    }
    public async Task<CheckoutResult> CalculateCheckoutAsync(Guid cartId, CancellationToken ct)
    {
        var cart = await _dbContext.Carts
            .Include(c => c.Items)
            .FirstOrDefaultAsync(c => c.Id == cartId, ct);
        if (cart is null) throw new InvalidOperationException("Cart not found");
        _logger.LogInformation("Calculating checkout for cart {CartId} with {ItemCount} items", 
            cartId, cart.Items.Count);
        var lineItems = new List<CheckoutLineItem>();
        var errors = new List<string>();
        foreach (var item in cart.Items)
        {
            // Parallel queries to different components
            var productTask = _catalogQueries.GetProductAsync(item.ProductId, ct);
            var stockTask = _inventoryQueries.GetStockAsync(item.ProductId, ct);
            var discountTask = _pricingQueries.GetBestDiscountAsync(item.ProductId, ct);
            await Task.WhenAll(productTask, stockTask, discountTask);
            var product = await productTask;
            var stock = await stockTask;
            var discount = await discountTask;
            if (product is null)
            {
                errors.Add($"Product {item.ProductId} no longer exists");
                continue;
            }
            if (stock?.AvailableQuantity < item.Quantity)
            {
                errors.Add($"Insufficient stock for {product.Name}. Available: {stock.AvailableQuantity}");
                continue;
            }
            var unitPrice = product.Price;
            var discountAmount = 0m;
            if (discount is not null)
            {
                unitPrice = discount.ApplyDiscount(product.Price);
                discountAmount = product.Price - unitPrice;
            }
            lineItems.Add(new CheckoutLineItem(
                item.ProductId,
                product.Name,
                item.Quantity,
                product.Price,
                unitPrice,
                discountAmount,
                product.Currency));
        }
        var subtotal = lineItems.Sum(i => i.DiscountedUnitPrice * i.Quantity);
        var tax = subtotal * 0.08m; // Simplified tax
        var shipping = subtotal > 100 ? 0 : 9.99m;
        _logger.LogInformation(
            "Checkout calculated: Subtotal={Subtotal}, Tax={Tax}, Shipping={Shipping}",
            subtotal, tax, shipping);
        return new CheckoutResult(
            cartId,
            lineItems,
            new Money(subtotal, "USD"),
            new Money(tax, "USD"),
            new Money(shipping, "USD"),
            new Money(subtotal + tax + shipping, "USD"),
            errors);
    }
}
public record CheckoutLineItem(
    Guid ProductId,
    string ProductName,
    int Quantity,
    decimal OriginalUnitPrice,
    decimal DiscountedUnitPrice,
    decimal DiscountAmount,
    string Currency);
public record CheckoutResult(
    Guid CartId,
    IReadOnlyList<CheckoutLineItem> Items,
    Money Subtotal,
    Money Tax,
    Money Shipping,
    Money Total,
    IReadOnlyList<string> Errors);

The API Host — Wiring Components

src/Ecommerce.API/Program.cs:

using Catalog.Component.Contracts;
using Catalog.Component.Persistence;
using Catalog.Component.Services;
using Checkout.Component.Persistence;
using Checkout.Component.Services;
using Inventory.Component.Contracts;
using Microsoft.EntityFrameworkCore;
using Pricing.Component.Contracts;
using Pricing.Component.Persistence;
using Scalar.AspNetCore;
using SharedKernel.Events;
var builder = WebApplication.CreateBuilder(args);
// ==================== DATABASES ====================
// Each component owns its schema, can be split to separate DBs later
builder.Services.AddDbContext<CatalogDbContext>(options =>
    options.UseNpgsql(
        builder.Configuration.GetConnectionString("CatalogDatabase"),
        b => b.MigrationsHistoryTable("__EFMigrationsHistory", "catalog")));
builder.Services.AddDbContext<PricingDbContext>(options =>
    options.UseNpgsql(
        builder.Configuration.GetConnectionString("PricingDatabase"),
        b => b.MigrationsHistoryTable("__EFMigrationsHistory", "pricing")));
builder.Services.AddDbContext<InventoryDbContext>(options =>
    options.UseNpgsql(
        builder.Configuration.GetConnectionString("InventoryDatabase"),
        b => b.MigrationsHistoryTable("__EFMigrationsHistory", "inventory")));
builder.Services.AddDbContext<CheckoutDbContext>(options =>
    options.UseNpgsql(
        builder.Configuration.GetConnectionString("CheckoutDatabase"),
        b => b.MigrationsHistoryTable("__EFMigrationsHistory", "checkout")));
// ==================== COMPONENT REGISTRATION ====================
// Components register their public contracts only
builder.Services.AddScoped<ICatalogQueries, CatalogQueries>();
builder.Services.AddScoped<IPricingQueries, PricingQueries>();
builder.Services.AddScoped<IInventoryQueries, InventoryQueries>();
builder.Services.AddScoped<CheckoutService>();
// Event bus (in-memory for monolith, can swap to Kafka/RabbitMQ later)
builder.Services.AddSingleton<IEventBus, InMemoryEventBus>();
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}
app.UseHttpsRedirection();
// ==================== ENDPOINTS ====================
app.MapPost("/api/carts", async (Guid customerId, CheckoutDbContext db, CancellationToken ct) =>
{
    var cart = new Checkout.Component.Domain.Cart(customerId);
    db.Carts.Add(cart);
    await db.SaveChangesAsync(ct);
    return Results.Created($"/api/carts/{cart.Id}", cart.Id);
});
app.MapPost("/api/carts/{cartId:guid}/items", async (
    Guid cartId,
    Guid productId,
    int quantity,
    CheckoutDbContext db,
    ICatalogQueries catalog,
    CancellationToken ct) =>
{
    var cart = await db.Carts.FindAsync(new object[] { cartId }, ct);
    if (cart is null) return Results.NotFound();
    var product = await catalog.GetProductAsync(productId, ct);
    if (product is null) return Results.BadRequest(new { error = "Product not found" });
    cart.AddItem(productId, product.Name, quantity, product.Price, product.Currency);
    await db.SaveChangesAsync(ct);
    return Results.Ok();
});
app.MapPost("/api/carts/{cartId:guid}/checkout", async (
    Guid cartId,
    CheckoutService checkout,
    CancellationToken ct) =>
{
    var result = await checkout.CalculateCheckoutAsync(cartId, ct);
    return Results.Ok(result);
});
// Ensure databases
using (var scope = app.Services.CreateScope())
{
    await scope.ServiceProvider.GetRequiredService<CatalogDbContext>().Database.MigrateAsync();
    await scope.ServiceProvider.GetRequiredService<PricingDbContext>().Database.MigrateAsync();
    await scope.ServiceProvider.GetRequiredService<InventoryDbContext>().Database.MigrateAsync();
    await scope.ServiceProvider.GetRequiredService<CheckoutDbContext>().Database.MigrateAsync();
}
app.Run();

The Migration Path: When to Actually Extract

Shopify’s architecture isn’t “monolith forever.” It’s “monolith until the pain justifies extraction.”

Shopify’s extraction criteria (from their engineering blog):

  1. Independent failure domain — if this component fails, the rest must survive
  2. Different scaling needs — needs 10x more resources than other components
  3. Regulatory boundary — PCI, HIPAA, or other compliance requirements
  4. Team autonomy — team needs to deploy independently multiple times per day

Even then, extraction is gradual:

Monolith → Internal Library → Separate Process (same deploy) → Separate Service

Key Takeaways

  1. Shopify runs a $100B business on a modular monolith. Microservices aren’t the only path to scale.
  2. Extract by business capability, not technical function. The shipping calculator failed because it needed order data. A “payments” service succeeds because it owns the payment lifecycle.
  3. Components communicate via events, not database sharing. This boundary is what makes future extraction possible.
  4. Database per component from day one. Even in a shared PostgreSQL instance, separate schemas enforce ownership.
  5. The compiler enforces boundaries. No using Checkout.Component.Internal in Catalog.Component.
  6. Extract when you have pain, not when you have a blog post. Shopify extracted their storefront renderer (different scaling) and their search (different technology). Not their checkout.

References

The next time someone tells you “monoliths don’t scale,” point them to Shopify. The architecture isn’t the bottleneck — the boundaries are. Build components with clear contracts, communicate via events, and extract only when the business case is undeniable.

Components, not services. Boundaries, not distributed systems. Scale through clarity. 🏗️


메타데이터
post_id
0e4a050b9e4b
slug
why-shopify-rejected-microservices-and-what-they-did-instead-0e4a050b9e4b
url
https://medium.com/startup-insider-edge/why-shopify-rejected-microservices-and-what-they-did-instead-0e4a050b9e4b
canonical_url
https://medium.com/startup-insider-edge/why-shopify-rejected-microservices-and-what-they-did-instead-0e4a050b9e4b
author_url
https://medium.com/@mariammaurice
status
ok
fetched_at
2026-07-08 20:12:56