
We’ve finally reached the last post of this series! So far, we’ve explored the entire backend design process, from domain modeling to handling persistence, transactions, security, APIs, and the gateway. All these layers were containerized with Docker, working together as a single, cohesive system.
Now it’s time to put that backend to use. This post is dedicated entirely to the frontend—specifically, a Single Page Application (SPA) built with Angular and how it consumes the backend in this project.
- Part 1 - EcommerceDDD overview
- Part 2 - Strategic and Tactical Design
- Part 3 - Domain events and Event Sourcing
- Part 4 - Implementing Event Sourcing with MartenDB
- Part 5 - Wrapping up infrastructure (Docker, API gateway, IdentityServer, Kafka)
- EcommerceDDD++: Streamlining API Client Generation with Kiota and Koalesce
Why Angular and Typescript
As a C# developer, TypeScript felt instantly familiar. TypeScript’s C#-like syntax, strong typing, and excellent tooling made it an easy recommendation for backend developers who want to explore frontend development, regardless of the framework, as long as it supports TypeScript.
For this project, I chose Angular for a few key reasons:
- It’s one of the most popular frameworks for building modern web apps.
- It provides a well-structured architectural blueprint and an excellent CLI.
- It’s built with TypeScript in mind and is opinionated—something I appreciate in large projects.
While Angular’s structure comes with a learning curve, it promotes consistency and maintainability across the codebase.
Development tools
Angular’s CLI is incredibly powerful, and you can scaffold an entire application with just a few commands. It helps generate modules, components, services, directives, routing configurations, and more. I highly recommend checking out Angular’s documentation if you’re just getting started.
For development, I strongly recommend using Visual Studio Code. Its extensibility through extensions makes it one of the best code editors available for web development. Whether you need code snippets, linters, formatting tools, or icons, there’s likely an extension that fits the bill.
Architecture
The frontend architecture is simple. You can learn more about each component by checking the documentation. I’ll highlight what matters for us in the structure below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
├── app
│ ├─ clients
│ │ ├── api
│ │ ├── customerManagement
│ │ ├── inventoryManagement
│ │ ├── orderProcessing
│ │ ├── productCatalog
│ │ ├── quoteManagement
│ │ └── models
│ ├─ core
│ | ├── guards
| | ├── interceptors
| | ├── pipes
| | └── services
│ └── api
├───├─ features
│ | ├── authentication
│ | └── ecommerce
│ | ├── constants
│ | └── services
├───├─ shared
│ | ├── services
│ | └── components
└───
- clients: Contains the Kiota-generated API clients based on the Gateway’s OpenAPI specification, which exposes paths and ViewModels mapped from C# to TypeScript equivalents. These clients are kept up to date whenever API contracts change (routes, produced types such as ViewModels, etc). Kiota generates one subfolder per service (e.g.
quoteManagement,orderProcessing) plus a sharedmodelsfolder containing all response/request types. - core: Contains reusable code shared across the application — such as HTTP interceptors, pipes, notification services, local storage handlers, and token utilities. It’s fully decoupled from feature-specific modules. The
core/services/apisubfolder contains thin domain API services (e.g.QuoteApiService,OrderApiService) that wrap the generated Kiota client, ensuring no component ever accesses it directly. - features: The features folder is where the different logical sections of the application live. I see the
user accountcreation andloginas a public section to be exposed without needing authentication, but as a preceding step. I grouped them into theauthenticationfeature. Everything else assumes the user is authenticated and goes inside theecommercefeature. You can organize your features as you see fit for your needs. - features.authentication: Components to handle customer’s account creation and login, as a public area of the application.
- features.ecommerce:
- cart, orders, product-selection, customer-details, …: UI components live directly under
ecommerce, each in its own folder. Note that some names may differ from backend terminology — for example, the cart in the frontend aligns with quote management on the backend. - constants: The constants used throughout this feature to avoid magic numbers or strings. It’s a best practice for maintainability.
- services: Injectable services used across the feature.
- cart, orders, product-selection, customer-details, …: UI components live directly under
- shared: Shared is where generic services and components live to support the overall app.
⚠️ Merging OpenAPI specifications is not something you get for free. I developed and used the Koalesce .NET library to streamline this at the Gateway level. Every time you hit the Gateway, Koalesce merges all endpoints I allowed to be exposed to the external world as a single OpenAPI specification. I dedicated an entire post to this topic — Streamlining API Client Generation with Kiota and Koalesce — if you want a deep dive.
With all services up and running, generating an API client using Kiota is fairly simple, and I set it up out of the box with a Docker profile that you can easily run whenever you need to regenerate clients:
1
docker-compose --profile tools run regenerate-clients
Run SPA, run!
To run the app locally, you need Node.js installed. All third-party dependencies are listed in package.json.
1
npm install
⚠️ The node_modules folder is excluded from version control via .gitignore and should never be committed. Always fetch fresh packages from upstream to benefit from bug and security updates.
Once the backend is running with Docker (docker-compose up or from Visual Studio), start the Angular SPA with:
1
ng serve
This will compile the project and launch a dev server on http://localhost:4200. Angular’s live reload feature ensures a smooth development experience.
⚠️ When preparing for production, don’t forget to bundle and optimize your app using the Angular CLI’s build tools to minimize load time and asset sizes.
Note: as I mentioned in the earlier post, you can run both the backend and the frontend at once in Docker with the command below if you don’t need to debug the frontend.
1
docker compose --profile frontend up
Commanding changes, Querying data

Now, let’s take, for example, adding a product to the cart, and this extends to updating quantities or removing the product.
Components never access the Kiota-generated client directly. Instead, they inject a dedicated domain API service — in this case QuoteApiService — which encapsulates all the HTTP calls for the quote management context. In the cart.component.ts you’ll see:
1
2
3
4
5
6
7
8
9
10
11
try {
await this.quoteApiService.addItem(
this.quote?.quoteId!,
product.productId!,
product.quantityAddedToCart);
await this.getOpenQuote();
} catch (error) {
this.quoteApiService.handleError(error);
} finally {
this.loaderService.setLoading(false);
}
As previously discussed, the architecture follows CQRS: commands mutate state, queries fetch data.
Under the hood, QuoteApiService.addItem issues a PUT request to the QuoteController, where the endpoint handles the AddQuoteItem command to apply the changes to a given Quote:
1
2
3
4
5
6
7
8
9
10
11
12
13
[HttpPut, Route("{quoteId:guid}/items")]
[MapToApiVersion(ApiVersions.V2)]
[Authorize(Roles = Roles.Customer, Policy = Policies.CanWrite)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> AddItem([FromRoute] Guid quoteId,
[FromBody] AddQuoteItemRequest request, CancellationToken cancellationToken) =>
await Response(
AddQuoteItem.Create(
QuoteId.Of(quoteId),
ProductId.Of(request.ProductId),
request.Quantity
), cancellationToken
);
Commands are imperative and ideally should not return data, which is a job for queries. A command is a one-way operation. When executing a command, I only verify whether the operation succeeded.
If everything goes well, a GET request is made via QuoteApiService.getOpenQuote() to execute the GetCustomerOpenQuote query that brings the Quote with the latest data:
1
await this.quoteApiService.getOpenQuote();
1
2
3
4
5
6
7
8
[HttpGet]
[MapToApiVersion(ApiVersions.V2)]
[Authorize(Roles = Roles.Customer, Policy = Policies.CanRead)]
[ProducesResponseType(typeof(QuoteViewModel), StatusCodes.Status200OK)]
public async Task<IActionResult> GetOpenQuote(CancellationToken cancellationToken) =>
await Response(
GetCustomerOpenQuote.Create(), cancellationToken
);
With the result in hand, the UI can update accordingly. Everything else in the project works pretty much the same way.
Real-time notifications with SignalR

SignalR is a real-time communication library for ASP.NET Core that enables server-side code to push content to connected clients instantly over WebSockets, falling back to other protocols when needed.
Why SignalR?
- Avoids constant polling from the frontend.
- Reduces unnecessary API calls and latency.
- Enables a reactive, event-driven UI.
In a distributed system like this, real-time updates significantly enhance the user experience, especially in scenarios such as order tracking. I integrated SignalR to broadcast updates from the backend to the SPA frontend when certain key events happen. In the EcommerceDDD.OrderProcessing microservice, as an order progresses through states such as Placed, Processed, Paid, or Shipped, each related handler updates the order status in real time.
Each related handler depends on IOrderNotificationService, a service wrapper that internally calls the crosscutting EcommerceDDD.SignalR API controller, which broadcasts the order status through OrderStatusHub.cs. The handlers never access the Kiota-generated client directly — the wrapper encapsulates that. Here’s an example:
1
2
3
4
5
6
7
// Updating order status on the UI with SignalR
await _orderNotificationService.UpdateOrderStatusAsync(
order.CustomerId.Value,
order.Id.Value,
order.Status.ToString(),
(int)order.Status,
cancellationToken);
If you place an Order and keep watching it, you’ll notice the status changing. On the frontend side of things, the orders.component.ts connects to the hub and reacts to its pushed notifications thanks to the @microsoft/signalr package, like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private async addCustomerToSignalrGroup() {
if (!this.authService.currentCustomer) return;
const conn = this.signalrService.connection;
if (conn.state === 'Disconnected') {
try {
await conn.start();
console.log('SignalR Connected!');
} catch (err) {
console.error(err);
return;
}
}
if (conn.state === 'Connected') {
conn.invoke('JoinCustomerToGroup', this.authService.currentCustomer!.id);
}
}
Final thoughts
That wraps up the series. What started as an exploration of DDD and Event Sourcing grew into a full-stack reference project: bounded contexts modeled with aggregates and domain events, persistence and projections with MartenDB, distributed workflow coordination with Kafka and the SAGA pattern, reliable event delivery through the Outbox Pattern, and now an Angular SPA that consumes it all through strongly-typed generated clients and real-time SignalR notifications.
The goal was never to prescribe a perfect architecture, but to demonstrate how these patterns interact in a realistic setting. Microservices and DDD carry real costs — they pay off when the domain complexity and team structure justify them. Treat this project as a reference, not a template.
I intend to keep the project evolving. If the series was useful to you, share it or drop feedback — I’d genuinely like to hear what you’d do differently.