Project & Domain Model Overview
Welcome to the ASP.NET Parcel Tracking API course! In this course, you will build a fully functional REST API that models how real-world carriers like UPS, FedEx, and USPS track parcels from pickup to delivery.
What We're Building
The Parcel Tracking API handles the complete lifecycle of a package:
- Register a parcel with shipper and recipient addresses
- Generate a tracking number and assign a service type
- Record tracking events as the parcel moves through the carrier network
- Update parcel status based on business rules
- Confirm delivery with a signature and recipient name
- Handle exceptions like failed delivery attempts and returns
By the end of this course, you will have a production-quality API that supports search, filtering, pagination, analytics, and robust error handling.
Why This Domain?
Parcel tracking is an excellent domain for learning API design because it includes:
- Multiple related entities with different relationship types
- A state machine (parcel status lifecycle) with business rules
- Temporal data (timestamps, estimated dates, event histories)
- Real-world constraints (tracking numbers must be unique, addresses require validation)
- Optional relationships (delivery confirmation only exists for delivered parcels)
Real carrier APIs like the UPS Tracking API and FedEx Track API use very similar data structures. The model we build here is inspired by their public documentation.
The Domain Model
Our API has six core entities and several supporting enums.
Entities Overview
1┌──────────────────┐ ┌──────────────────┐2│ Address │ │ Address │3│ (Shipper) │ │ (Recipient) │4└────────┬─────────┘ └────────┬─────────┘5 │ 1 │ 16 │ │7 ▼ ▼8┌──────────────────────────────────────────────┐9│ Parcel │10│ TrackingNumber, Status, ServiceType, ... │11├──────────────────────────────────────────────┤12│ Weight, Dimensions, DeclaredValue │13│ EstimatedDeliveryDate, DeliveryAttempts │14└──┬───────┬──────────────┬────────┬───────────┘15 │ 1..* │ 1..* │ 0..1 │ *16 ▼ ▼ ▼ ▼17┌────────────────┐ ┌────────────────┐ ┌───────────────────────┐18│ TrackingEvent │ │ParcelContent │ │ DeliveryConfirmation │19│ Timestamp,Type │ │Item: HsCode, │ │ ReceivedBy, Signature │20│ Location, etc. │ │Quantity, Value │ │ DeliveredAt │21└────────────────┘ └────────────────┘ └───────────────────────┘2223 ┌──────────────────┐24 │ ParcelWatcher │25 │ Email, Name │26 │ CreatedAt │27 └──────────────────┘28 * ↕ *29 (many-to-many with Parcel)
Parcel
The Parcel entity is the central aggregate. It tracks everything about a package:
- Identity: A unique
Id(GUID) and a human-readableTrackingNumber(e.g.,PKG-20250215-A1B2C3) - Service: The
ServiceType(Standard, Express, Overnight, Economy) determines delivery speed - Status: The current
ParcelStatusreflects where the parcel is in its lifecycle - Addresses: Every parcel has a shipper (sender) and a recipient, each stored as an
Address - Physical properties:
Weight,WeightUnit,Length,Width,Height,DimensionUnit - Value:
DeclaredValueandCurrencyfor insurance purposes - Dates:
EstimatedDeliveryDate,ActualDeliveryDate,CreatedAt,UpdatedAt - Delivery tracking:
DeliveryAttemptscounts how many times delivery was attempted
Address
The Address entity stores location information for both shippers and recipients:
- Location fields:
Street1,Street2,City,State,PostalCode,CountryCode - Contact fields:
ContactName,CompanyName,Phone,Email - Classification:
IsResidentialdistinguishes between residential and commercial addresses
An address can be referenced by multiple parcels. For example, a warehouse address might be the shipper for thousands of parcels.
TrackingEvent
A TrackingEvent records a single point in the parcel's journey:
- When:
Timestampof the event - What:
EventTypedescribes what happened (e.g., PickedUp, ArrivedAtFacility, OutForDelivery) - Where:
LocationCity,LocationState,LocationCountry - Details:
Descriptionprovides a human-readable summary - Exceptions:
DelayReasonexplains why if there was a delay
A parcel accumulates tracking events over time. Querying them in chronological order gives you the full shipment history, just like on a carrier's tracking page.
DeliveryConfirmation
The DeliveryConfirmation entity is created only when a parcel is delivered:
- Who:
ReceivedByrecords who signed for the package - Where:
DeliveryLocation(e.g., "Front door", "Reception desk") - Proof:
SignatureImagestores the signature data - When:
DeliveredAtis the confirmed delivery timestamp
This is a one-to-one optional relationship: a parcel might not have a delivery confirmation yet.
ParcelContentItem
The ParcelContentItem entity describes what is inside a parcel. Real-world carrier APIs (FedEx, UPS) require structured customs declarations for international shipments, and each item in the parcel must be declared separately with its HS (Harmonized System) code.
- HS Code: A 6-digit code in the format
XXXX.XXthat classifies the item for customs (e.g.,8471.30for laptops) - Description: What the item is (e.g., "Laptop computer")
- Quantity: How many units of this item
- Unit Value: The value of a single unit, with a currency code (ISO 4217, 3 characters)
- Weight: The weight of this item, with a unit (Lb or Kg)
- Country of Origin: ISO 3166-1 alpha-2 country code where the item was manufactured (e.g.,
CN,US,DE)
A parcel can contain multiple content items. For example, a parcel might contain 2 laptops and 5 phone cases, each declared as a separate content item with its own HS code and value.
ParcelWatcher
The ParcelWatcher entity represents a person who wants to receive notifications about one or more parcels:
- Contact:
Email(required) and an optionalName - Audit:
CreatedAtrecords when the watcher was added - Relationship: A many-to-many relationship with
Parcel— one watcher can follow multiple parcels, and one parcel can have multiple watchers
This is the first many-to-many relationship in our domain. EF Core handles it by auto-generating a join table. You do not need to create the join entity yourself.
Enums
Enums define the fixed sets of values used throughout the domain.
ParcelStatus
Represents the lifecycle states of a parcel:
| Value | Description |
|---|---|
LabelCreated | Shipping label generated, awaiting pickup |
PickedUp | Carrier has the parcel |
InTransit | Moving through the carrier network |
OutForDelivery | On the delivery vehicle |
Delivered | Successfully delivered |
Exception | Problem occurred (failed attempt, damage, etc.) |
Returned | Returned to sender |
ServiceType
Defines the shipping speed:
| Value | Typical Delivery |
|---|---|
Economy | 5-7 business days |
Standard | 3-5 business days |
Express | 1-2 business days |
Overnight | Next business day |
Supporting Enums
EventType: Categorizes tracking events (LabelCreated, PickedUp, ArrivedAtFacility, DepartedFacility, InTransit, OutForDelivery, Delivered, DeliveryAttempted, Exception, Returned, etc.)ExceptionReason: Describes why an exception occurred (AddressNotFound, RecipientUnavailable, DamagedInTransit, WeatherDelay, CustomsHold, RefusedByRecipient)WeightUnit: Pounds (Lb) or Kilograms (Kg)DimensionUnit: Inches (In) or Centimeters (Cm)
Entity Relationships
Understanding the relationships is critical for configuring EF Core correctly:
| Relationship | Type | Details |
|---|---|---|
| Parcel -> Shipper Address | Many-to-one | ShipperAddressId foreign key |
| Parcel -> Recipient Address | Many-to-one | RecipientAddressId foreign key |
| Parcel -> TrackingEvents | One-to-many | ParcelId foreign key on TrackingEvent |
| Parcel -> ParcelContentItems | One-to-many | ParcelId foreign key on ParcelContentItem |
| Parcel -> DeliveryConfirmation | One-to-one (optional) | ParcelId foreign key on DeliveryConfirmation |
| Parcel <-> ParcelWatcher | Many-to-many | Auto-generated join table |
The Address entity is shared: a single address row can be the shipper for one parcel and the recipient for another. This avoids duplicating address data.
Technology Stack
For this project, we use:
- ASP.NET Core 10 with the minimal API or controller pattern
- Entity Framework Core 10 for data access
- PostgreSQL as the database, running via Docker for easy local setup
- OpenAPI with Scalar for interactive API documentation and testing
- API Key Authentication to secure endpoints, matching real carrier API patterns
- C# 14 with nullable reference types enabled
PostgreSQL is a production-grade relational database with native support for UUIDs, precise numeric types, and advanced querying features. Running it in a Docker container keeps local setup simple: a single docker run command gives you a ready-to-use database server.
Solution Structure
We organize the code as a multi-project solution following Clean Architecture principles. Instead of putting everything in a single Web API project, we split the code into four projects with strict dependency rules:
1ParcelTracking/2├── ParcelTracking.sln3└── src/4 ├── ParcelTracking.Domain/ # Entities, enums, business rules5 ├── ParcelTracking.Application/ # Services, interfaces, DTOs, validators6 ├── ParcelTracking.Infrastructure/ # EF Core DbContext, configurations, migrations, seeders7 └── ParcelTracking.Api/ # Controllers, authentication, middleware, Program.cs
Why Multiple Projects?
A single-project structure works for small demos, but production APIs benefit from separation:
- Dependency direction is enforced at compile time. Domain has zero dependencies. Application depends only on Domain. Infrastructure depends on Application and Domain. Api depends on all three. You cannot accidentally reference EF Core from a domain entity because the Domain project does not have that package.
- Testability improves. You can unit-test domain logic and application services without a database or web server.
- Teams can work in parallel. Changing a controller does not force recompilation of the domain model.
Project References
1Api ──→ Infrastructure ──→ Application ──→ Domain2 │ ↑3 └──────────→ Application ───────────────────┘
Each arrow represents a dotnet add reference between projects. The Api project also references Application directly so it can use DTOs and service interfaces.
Summary
In this lesson, you learned:
- The Parcel Tracking API models a real-world carrier system with six core entities
Parcelis the central entity, linked to addresses, tracking events, content items, an optional delivery confirmation, and parcel watchers- Enums define fixed value sets for status, service type, event types, and units of measurement
- Entity relationships include many-to-one (addresses), one-to-many (tracking events, content items), one-to-one optional (delivery confirmation), and many-to-many (parcel watchers)
- We use ASP.NET Core 10 with EF Core and PostgreSQL for development
Next, we will dive deep into designing the entity classes, their properties, and the enum definitions.