Discount service
Discount stores one coupon per product name and serves it over gRPC. It has no REST controllers. Its only client in the codebase is the Basket service.
| Source | src/Services/Discount |
| Store | PostgreSQL, Coupon table, accessed with Npgsql and Dapper |
| Protocol | gRPC over HTTP/2 cleartext, Kestrel endpoint http://0.0.0.0:8080 |
| Contract | src/Services/Discount/Discount.Application/Protos/discount.proto |
| Local port | 8002 → container 8080 |
Layers
Section titled “Layers”| Project | What is in it |
|---|---|
src/Services/Discount/Discount.API |
DiscountService : DiscountProtoServiceBase, which maps each RPC to a mediator request; Program.cs runs the DB migration at startup |
src/Services/Discount/Discount.Application |
The .proto (compiled with GrpcServices="Server"), commands, a query, handlers, DiscountMapper |
src/Services/Discount/Discount.Core |
Coupon entity, IDiscountRepository |
src/Services/Discount/Discount.Infrastructure |
DiscountRepository (Dapper SQL), DbExtension.MigrateDatabase |
gRPC contract
Section titled “gRPC contract”service DiscountProtoService { rpc GetDiscount(GetDiscountRequest) returns (CouponModel); rpc CreateDiscount(CreateDiscountRequest) returns (CouponModel); rpc UpdateDiscount(UpdateDiscountRequest) returns (CouponModel); rpc DeleteDiscount(DeleteDiscountRequest) returns (DeleteDiscountResponse);}
message CouponModel { int32 id = 1; string productName = 2; string description = 3; int32 amount = 4;}| RPC | Mediator request | Repository SQL |
|---|---|---|
GetDiscount |
GetDiscountQuery |
SELECT * FROM Coupon WHERE ProductName = @ProductName |
CreateDiscount |
CreateDiscountCommand |
INSERT INTO Coupon (ProductName, Description, Amount) ... |
UpdateDiscount |
UpdateDiscountCommand |
UPDATE Coupon SET ... WHERE Id = @Id |
DeleteDiscount |
DeleteDiscountCommand |
DELETE FROM Coupon WHERE ProductName = @ProductName |
All queries are parameterized through Dapper (src/Services/Discount/Discount.Infrastructure/Repositories/DiscountRepository.cs). Each call opens a new NpgsqlConnection, and Npgsql connection pooling makes that cheap.
Amount is an int32, so discounts are whole currency units. Basket subtracts it from the unit price.
Data model
Section titled “Data model”erDiagram
COUPON {
serial Id PK
varchar ProductName "VARCHAR(500) NOT NULL"
text Description
int Amount
}
Startup migration
Section titled “Startup migration”app.MigrateDatabase<Program>() (src/Services/Discount/Discount.Infrastructure/Extensions/DbExtension.cs) runs before the host starts:
- Connects to the
postgresmaintenance database and creates the target database ifpg_databasedoes not list it. - In one transaction, takes a PostgreSQL advisory lock (
pg_advisory_xact_lock) so replicas starting together cannot race, then runsCREATE TABLE IF NOT EXISTS Coupon(...)and a unique index onProductName. - Seeds two coupons,
ASUS ZenBook 13 OLED Ultrabook(500) andASUS ROG Zephyrus G14 Gaming Laptop(700), only when the table is empty, withINSERT ... ON CONFLICT DO NOTHING. Seed coupons an admin deleted do not come back. - Retries transient connection failures with exponential back-off and fails fast (the process exits) if the database is still unreachable, instead of starting a broken service.
Implementation notes
Section titled “Implementation notes”- Missing coupons are not errors.
GetDiscountQueryHandlerthrowsRpcException(StatusCode.NotFound)when the repository returnsnull. The repository never does: it returns a placeholderCoupon { ProductName = "No Discount", Amount = 0 }. The effective behaviour is “no coupon means a zero discount”, which suits Basket, but the NotFound branch is dead code. - Mapping.
DiscountMapperis a Mapperly source-generated mapper registered as a singleton.CreateignoresId, because the database generates it. - Handlers return protobuf types. Handlers return
CouponModel(a generated gRPC type), soDiscount.Applicationdepends on the transport contract. - Nothing reaches it over HTTP. The Ocelot route files still contain
/DiscountREST routes, anddeploy/istio/virtualservices.yamlhas a discount VirtualService matching/api/v1/Discount. Discount has no REST controllers, so these routes cannot be served. Coupon administration would need a REST facade, gRPC-JSON transcoding, or a gRPC-capable gateway route. - No tracing of Npgsql. Only ASP.NET Core instrumentation is registered, so incoming gRPC calls are traced, but database calls are not.