12 minlesson

Project & Domain Model Overview

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:

  1. Register a parcel with shipper and recipient addresses
  2. Generate a tracking number and assign a service type
  3. Record tracking events as the parcel moves through the carrier network
  4. Update parcel status based on business rules
  5. Confirm delivery with a signature and recipient name
  6. 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 │ 1
6 │ │
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└────────────────┘ └────────────────┘ └───────────────────────┘
22
23 ┌──────────────────┐
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-readable TrackingNumber (e.g., PKG-20250215-A1B2C3)
  • Service: The ServiceType (Standard, Express, Overnight, Economy) determines delivery speed
  • Status: The current ParcelStatus reflects 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: DeclaredValue and Currency for insurance purposes
  • Dates: EstimatedDeliveryDate, ActualDeliveryDate, CreatedAt, UpdatedAt
  • Delivery tracking: DeliveryAttempts counts 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: IsResidential distinguishes 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: Timestamp of the event
  • What: EventType describes what happened (e.g., PickedUp, ArrivedAtFacility, OutForDelivery)
  • Where: LocationCity, LocationState, LocationCountry
  • Details: Description provides a human-readable summary
  • Exceptions: DelayReason explains 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: ReceivedBy records who signed for the package
  • Where: DeliveryLocation (e.g., "Front door", "Reception desk")
  • Proof: SignatureImage stores the signature data
  • When: DeliveredAt is 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.XX that classifies the item for customs (e.g., 8471.30 for 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 optional Name
  • Audit: CreatedAt records 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:

ValueDescription
LabelCreatedShipping label generated, awaiting pickup
PickedUpCarrier has the parcel
InTransitMoving through the carrier network
OutForDeliveryOn the delivery vehicle
DeliveredSuccessfully delivered
ExceptionProblem occurred (failed attempt, damage, etc.)
ReturnedReturned to sender

ServiceType

Defines the shipping speed:

ValueTypical Delivery
Economy5-7 business days
Standard3-5 business days
Express1-2 business days
OvernightNext 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:

RelationshipTypeDetails
Parcel -> Shipper AddressMany-to-oneShipperAddressId foreign key
Parcel -> Recipient AddressMany-to-oneRecipientAddressId foreign key
Parcel -> TrackingEventsOne-to-manyParcelId foreign key on TrackingEvent
Parcel -> ParcelContentItemsOne-to-manyParcelId foreign key on ParcelContentItem
Parcel -> DeliveryConfirmationOne-to-one (optional)ParcelId foreign key on DeliveryConfirmation
Parcel <-> ParcelWatcherMany-to-manyAuto-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.sln
3└── src/
4 ├── ParcelTracking.Domain/ # Entities, enums, business rules
5 ├── ParcelTracking.Application/ # Services, interfaces, DTOs, validators
6 ├── ParcelTracking.Infrastructure/ # EF Core DbContext, configurations, migrations, seeders
7 └── 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 ──→ Domain
2 │ ↑
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
  • Parcel is 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.