Skip to main content
The Catalog and Basket services use PostgreSQL with Marten as a document database, providing the flexibility of NoSQL with the reliability of PostgreSQL.

Why Marten?

Marten transforms PostgreSQL into a document database:
  • Document Storage: Store objects as JSON documents
  • LINQ Queries: Query documents using familiar LINQ syntax
  • ACID Transactions: Full PostgreSQL transactional support
  • Event Sourcing: Built-in event store capabilities
  • Schema Management: Automatic schema generation and migrations

Catalog Service Configuration

The Catalog service uses Marten for product catalog management.

Connection String

src/Services/Catalog/Catalog.API/appsettings.json
{
  "ConnectionStrings": {
    "Database": "Server=localhost;Port=5432;Database=CatalogDb;User Id=postgres;Password=postgres;Include Error Detail=true"
  }
}

Service Registration

src/Services/Catalog/Catalog.API/Program.cs
builder.Services.AddMarten(opts =>
{
    opts.Connection(builder.Configuration.GetConnectionString("Database")!);
}).UseLightweightSessions();

if (builder.Environment.IsDevelopment())
    builder.Services.InitializeMartenWith<CatalogInitialData>();
Key Points:
  • UseLightweightSessions(): Optimized for read-heavy workloads
  • InitializeMartenWith<T>: Seeds initial data in development

Document Models

Marten stores domain objects as JSON documents:
public class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; } = default!;
    public List<string> Category { get; set; } = new();
    public string Description { get; set; } = default!;
    public string ImageFile { get; set; } = default!;
    public decimal Price { get; set; }
}

Data Seeding

src/Services/Catalog/Catalog.API/Data/CatalogInitialData.cs
public class CatalogInitialData : IInitialData
{
    public async Task Populate(IDocumentStore store, CancellationToken cancellation)
    {
        using var session = store.LightweightSession();

        if (await session.Query<Product>().AnyAsync())
            return;

        // Marten UPSERT will cater for existing records
        session.Store<Product>(GetPreconfiguredProducts());
        await session.SaveChangesAsync();
    }

    private static IEnumerable<Product> GetPreconfiguredProducts() => new List<Product>()
    {
        new Product()
        {
            Id = new Guid("5334c996-8457-4cf0-815c-ed2b77c4ff61"),
            Name = "IPhone X",
            Description = "This phone is the company's biggest change to its flagship smartphone in years.",
            ImageFile = "product-1.png",
            Price = 950.00M,
            Category = new List<string> { "Smart Phone" }
        },
        // More products...
    };
}

Basket Service Configuration

The Basket service uses Marten for shopping cart persistence with Redis caching.

Connection String

src/Services/Basket/Basket.API/appsettings.json
{
  "ConnectionStrings": {
    "Database": "Server=localhost;Port=5433;Database=BasketDb;User Id=postgres;Password=postgres;Include Error Detail=true",
    "Redis": "localhost:6379"
  }
}

Service Registration

src/Services/Basket/Basket.API/Program.cs
builder.Services.AddMarten(opts =>
{
    opts.Connection(builder.Configuration.GetConnectionString("Database")!);
    opts.Schema.For<ShoppingCart>().Identity(x => x.UserName);
}).UseLightweightSessions();

builder.Services.AddScoped<IBasketRepository, BasketRepository>();
builder.Services.Decorate<IBasketRepository, CachedBasketRepository>();
Key Points:
  • Custom identity: UserName instead of default Id
  • Decorator pattern: Adds caching layer over repository

Repository Implementation

src/Services/Basket/Basket.API/Data/BasketRepository.cs
public class BasketRepository(IDocumentSession session)
    : IBasketRepository
{
    public async Task<ShoppingCart> GetBasket(string userName, CancellationToken cancellationToken = default)
    {
        var basket = await session.LoadAsync<ShoppingCart>(userName, cancellationToken);
        return basket is null ? throw new BasketNotFoundException(userName) : basket;
    }

    public async Task<ShoppingCart> StoreBasket(ShoppingCart basket, CancellationToken cancellationToken = default)
    {
        session.Store(basket);
        await session.SaveChangesAsync(cancellationToken);
        return basket;
    }

    public async Task<bool> DeleteBasket(string userName, CancellationToken cancellationToken = default)
    {
        session.Delete<ShoppingCart>(userName);
        await session.SaveChangesAsync(cancellationToken);
        return true;
    }
}

Working with Marten Sessions

Lightweight Sessions

Optimized for read-heavy operations:
public async Task<Product> GetProduct(Guid id)
{
    // Injected IDocumentSession
    return await _session.LoadAsync<Product>(id);
}

Querying Documents

// LINQ queries
var products = await _session
    .Query<Product>()
    .Where(p => p.Category.Contains("Smart Phone"))
    .OrderBy(p => p.Price)
    .ToListAsync();

// Compiled queries for better performance
var expensiveProducts = await _session
    .Query<Product>()
    .Where(p => p.Price > 500)
    .ToListAsync();

Storing Documents

// Single document
_session.Store(product);
await _session.SaveChangesAsync();

// Multiple documents
_session.Store(products);
await _session.SaveChangesAsync();

// Update (load, modify, save)
var product = await _session.LoadAsync<Product>(id);
product.Price = 999.99M;
await _session.SaveChangesAsync();

Health Checks

builder.Services.AddHealthChecks()
    .AddNpgSql(builder.Configuration.GetConnectionString("Database")!);
Access health status at /health endpoint.

Docker Compose Setup

postgresql_catalog:
  image: postgres:latest
  environment:
    - POSTGRES_USER=postgres
    - POSTGRES_PASSWORD=postgres
    - POSTGRES_DB=CatalogDb
  ports:
    - "5432:5432"
  volumes:
    - postgres_catalog:/var/lib/postgresql/data/

postgresql_basket:
  image: postgres:latest
  environment:
    - POSTGRES_USER=postgres
    - POSTGRES_PASSWORD=postgres
    - POSTGRES_DB=BasketDb
  ports:
    - "5433:5432"
  volumes:
    - postgres_basket:/var/lib/postgresql/data/

Performance Tips

Use Lightweight Sessions

For read-heavy operations:
.UseLightweightSessions()

Batch Operations

// Store multiple documents in one transaction
_session.Store(products);
await _session.SaveChangesAsync();

Compiled Queries

For frequently executed queries:
public static class CompiledQueries
{
    public static readonly Func<IQuerySession, string, Task<Product>> ProductByName =
        (session, name) => session.Query<Product>()
            .Where(p => p.Name == name)
            .FirstOrDefaultAsync();
}

Troubleshooting

Connection Issues

# Test PostgreSQL connection
psql -h localhost -p 5432 -U postgres -d CatalogDb

# Check if database exists
psql -h localhost -p 5432 -U postgres -c "\l"

Schema Updates

Marten auto-generates schema. To manually update:
await using var store = DocumentStore.For(connectionString);
await store.Storage.ApplyAllConfiguredChangesToDatabaseAsync();

Redis Caching

Add caching layer to Basket service

Marten Documentation

Official Marten documentation

Build docs developers (and LLMs) love