Building a Real Ecommerce Platform from Scratch

· 6 min read · #dotnet#angular#architecture

I recently built OctaBlitz, a PC and gaming ecommerce platform. Not a tutorial project with a fake checkout button, but a production-deployed store with real Stripe and PayPal integration, a Redis-backed shopping cart, order management, inventory tracking, and a full CI/CD pipeline. Here is what I learned building it.

The Architecture

The backend is .NET 10 with clean architecture split across five projects.

API Layer (Controllers, middleware, error handling)

Application Layer (Use-case services, DTOs, validators)

Domain Layer (Entities, interfaces, value objects)

Infrastructure Layer (EF Core repos, Redis, payment providers, auth)

Each use case gets its own service class. Instead of a ProductService with fifteen methods, there is a CreateProductService, a GetProductBySlugService, a SearchProductsService, and so on. Each class does one thing. This makes the codebase easy to navigate because you can search for the service name and land directly on the logic you need.

The frontend is Angular 21 with standalone components and Signals for state management. Routes are lazy-loaded by feature (products, cart, checkout, orders, account). The entire UI is mobile-first using Tailwind CSS.

Shopping Cart: Guest to Logged-In

The cart was one of the trickier pieces to get right. The core problem is that users start browsing as guests, add items to their cart, and then log in. If the cart disappears on login, you lose the sale.

The solution is a dual-mode cart. Guest users get a localStorage cart that works immediately without any API calls. When they log in, the frontend sends the localStorage cart to the server, the server merges it with any existing server-side cart, and the localStorage cart is cleared. From that point the cart is Redis-backed and persists across sessions and devices.

The merge logic handles conflicts: if the same product is in both carts, quantities are summed. If a product in the guest cart is out of stock on the server, it is dropped with a notification. This all happens silently on login.

Payment Integration: Strategy Pattern

OctaBlitz supports three payment providers: Stripe (credit/debit), PayPal, and a stub for development. All three implement the same interface.

public interface IPaymentProvider
{
    string ProviderName { get; }
    Task<PaymentResult> CreateCheckoutAsync(Order order, string returnUrl, string cancelUrl);
    Task<PaymentResult> HandleWebhookAsync(string payload, string signature);
    Task<PaymentResult> CapturePaymentAsync(string transactionId);
}

The checkout page lets the user pick their payment method. Based on the selection, the backend resolves the correct provider from DI and initiates the checkout. Both Stripe and PayPal use hosted checkout pages, which means OctaBlitz never touches raw card data. The user is redirected to Stripe or PayPal, completes payment there, and gets redirected back with a transaction ID.

Webhooks confirm the payment asynchronously. This is important because the redirect can fail (user closes the tab, network drops) but the webhook will still arrive. The webhook handler validates the event signature, matches it to the order, and transitions the order status from Pending to Paid.

The stub provider skips all of this and immediately returns success. During development I used it exclusively so I could test the full checkout flow without needing Stripe test cards or PayPal sandbox credentials.

Order Status Machine

Orders follow a strict status progression: Pending, Paid, Processing, Shipped, Delivered. Cancelled is a terminal state reachable from Pending or Paid.

The service validates every transition. You cannot ship a cancelled order. You cannot mark an unpaid order as processing. Each transition is an explicit method call, not a string comparison.

One detail that matters: prices are captured as snapshots at order time. When a customer places an order, the product name, SKU, and unit price are copied into the OrderItem record. If the product price changes later, it does not affect existing orders. This is a mistake I have seen in other projects where the order total recalculates from current product prices and suddenly the numbers do not match the payment.

Redis for Cart and Events

Redis serves two purposes. First, it backs the server-side shopping cart. Cart reads and writes are sub-millisecond because Redis is in-memory, and the cart data is naturally ephemeral (if Redis restarts, the user just re-adds items).

Second, Redis pub/sub handles domain events. When an order is placed, the API publishes an event to a Redis channel. A background subscriber picks it up and handles side effects like sending notifications. When inventory drops below the low-stock threshold, another event fires and the inventory subscriber logs the alert.

This keeps the main request path fast. The API creates the order, publishes the event, and returns the response. The notification and inventory logic runs asynchronously in a background worker.

Deployment Pipeline

The CI/CD pipeline uses GitHub Actions with two stages.

On every pull request: format check, linting, unit tests, and integration tests (using Testcontainers with a real PostgreSQL instance). If any step fails, the PR is blocked.

On merge to main: build three Docker images (API, frontend, nginx), push them to GitHub Container Registry, SSH into the DigitalOcean droplet, write environment variables from GitHub secrets, pull the new images, restart the containers, and run a health check through Cloudflare Tunnel.

The entire deployment takes about 3 minutes from merge to live. Cloudflare Tunnel means the droplet has no public ports exposed. All traffic routes through Cloudflare's edge network which handles TLS termination and DDoS protection.

Testing

The project has 46 backend tests split between unit tests (with fakes for repositories and cache) and integration tests (with Testcontainers spinning up a real PostgreSQL database). Playwright handles E2E testing for the full checkout flow: browse products, add to cart, go to checkout, complete payment, verify confirmation page.

The Playwright tests caught more real bugs than the unit tests. A unit test tells you that the cart merge logic works in isolation. A Playwright test tells you that when a guest adds 3 items, logs in, and checks out with Stripe, the order confirmation shows the right total. That end-to-end path has dozens of potential failure points that only surface when all the pieces run together.

What I Would Do Differently

If I started over, I would set up Playwright E2E tests from the first feature, not after building most of the app. Retrofitting E2E tests is painful because you need test data setup, authentication flows, and cleanup for every test. If I had built them alongside each feature, the test infrastructure would have grown incrementally.

I would also add product reviews and ratings early. They are a natural part of any ecommerce experience and affect the product detail page layout, search ranking, and admin moderation workflows. Adding them later means touching many existing components.

The codebase is live at octablitz.alvinalmodal.dev if you want to see the full experience. Browse products, add items to cart, and go through checkout. Stripe and PayPal are in sandbox mode so no real charges are made.

Alvin Almodal

Alvin Almodal

Cloud & Data Engineering Consultant. Your partner for cloud-native builds and data pipelines.