Skip to content

C4 Architecture Model - Petfolio Service

This document describes the architecture of the Petfolio Service using the C4 model (Context, Containers, Components, Code).

Table of Contents


Level 1: System Context Diagram

Shows how the Petfolio Service fits into the broader ecosystem and its external dependencies.

C4Context
    title System Context Diagram - Petfolio Service

    Person(petOwner, "Pet Owner", "Manages their pets' portfolios via web/mobile")
    Person(vetPro, "Veterinarian/Pet Professional", "Manages client animals and records")

    System(petfolioService, "Petfolio Service", "Manages pet portfolios, health records, and account information")

    System_Ext(mysql, "MySQL Database", "Stores accounts, users, animals, and weight records")
    System_Ext(oauth, "OAuth Providers", "Google, Microsoft, Facebook authentication (Planned)")

    Rel(petOwner, petfolioService, "Uses", "HTTPS/REST API")
    Rel(vetPro, petfolioService, "Uses", "HTTPS/REST API")
    Rel(petfolioService, mysql, "Reads from and writes to", "MySQL Protocol:3306")
    Rel(petfolioService, oauth, "Authenticates users via", "OAuth 2.0 (Future)")

    UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")

External Actors and Systems

Actor/System Description Protocol/Interface
Pet Owners Primary users managing their pets' health and information HTTPS REST API (Port 7195)
Veterinarians/Pet Professionals Manage client animals, track health records HTTPS REST API (Port 7195)
MySQL Database Primary data store for all entities MySQL Protocol (Port 3306)
OAuth Providers Future: Google, Microsoft, Facebook for authentication OAuth 2.0 (Planned)

Level 2: Container Diagram

Shows the applications and data stores that make up the Petfolio system.

C4Container
    title Container Diagram - Petfolio Service

    Person(user, "User", "Pet owner or professional")

    Container_Boundary(petfolioSystem, "Petfolio System") {
        Container(api, "API Application", ".NET 9, C#", "Provides REST API for managing pet portfolios. Four-layer clean architecture: API, Application, Domain, Infrastructure")

        ContainerDb(db, "Database", "MySQL 8.0+", "Stores accounts, users, animals, and weight records")
    }

    Rel(user, api, "Uses", "HTTPS/JSON (Port 7195)")
    Rel(api, db, "Reads/Writes", "MySQL Protocol (Port 3306)")

    UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="1")

Container Details

API Application (.NET 9)

Technology Stack:

  • ASP.NET Core 9
  • MediatR 13.1.0 (CQRS)
  • FluentValidation 12.1.0
  • AutoMapper 15.0.1
  • Entity Framework Core 9.0
  • Swashbuckle (Swagger/OpenAPI)

Architecture Layers:

  1. API Layer - Controllers, middleware, HTTP concerns
  2. Application Layer - CQRS (commands/queries), handlers, validators, DTOs
  3. Domain Layer - Entities, value objects, business logic (zero dependencies)
  4. Infrastructure Layer - EF Core, repositories, database access

Endpoints:

  • https://localhost:7195/api/Animals/*
  • https://localhost:7195/api/Accounts/*
  • https://localhost:7195/api/Users/*
  • https://localhost:7195/swagger/index.html (API documentation)

MySQL Database

Version: 8.0+ Provider: Pomelo.EntityFrameworkCore.MySql 9.0

Tables:

  • Accounts - Business accounts for pet owners/professionals
  • Users - User profiles with OAuth authentication info
  • Animals - Pet records (species, breed, age, etc.)
  • Weight - Historical weight records (owned by Animal)

Level 3: Component Diagram

Shows the major components within the API Application container.

3.1 Clean Architecture Layer Dependencies

graph TB
    subgraph "API Layer"
        A1[Animals Controller]
        A2[Accounts Controller]
        A3[Users Controller]
        A4[Exception Middleware]
        A5[Program.cs / DI Setup]
    end

    subgraph "Application Layer"
        B1[Commands & Queries]
        B2[MediatR Handlers]
        B3[FluentValidation Validators]
        B4[DTOs & AutoMapper]
        B5[Validation Behaviour Pipeline]
    end

    subgraph "Infrastructure Layer"
        C1[PetfolioDbContext EF Core]
        C2[Repository Implementations]
        C3[EF Core Configurations]
        C4[Unit of Work]
    end

    subgraph "Domain Layer CORE"
        D1[Entities Account, Animal, User]
        D2[Value Objects Email, Name, Weight]
        D3[Repository Interfaces]
        D4[Domain Events]
        D5[Result Pattern & Errors]
    end

    subgraph "External"
        E1[(MySQL Database)]
    end

    A1 --> B1
    A2 --> B1
    A3 --> B1
    A1 --> B2
    A2 --> B2
    A3 --> B2

    B1 --> D1
    B2 --> D1
    B2 --> D3
    B4 --> D1
    B4 --> D2

    C1 --> D1
    C2 --> D3
    C3 --> D1
    C3 --> D2

    C1 --> E1
    C2 --> C1
    C4 --> C1

    style D1 fill:#e1f5ff
    style D2 fill:#e1f5ff
    style D3 fill:#e1f5ff
    style D4 fill:#e1f5ff
    style D5 fill:#e1f5ff

3.2 API Layer Components

graph TB
    subgraph "API Layer - Controllers"
        AC[Animals Controller<br/>POST /Create<br/>GET /Get/{id}<br/>GET /GetAll]
        ACC[Accounts Controller<br/>POST /Create<br/>GET /Get/{id}<br/>GET /GetAll]
        UC[Users Controller<br/>GET /Get/{id}<br/>GET /GetAll]
    end

    subgraph "Middleware"
        EXC[Exception Handling Middleware<br/>Catches exceptions<br/>Returns ProblemDetails]
    end

    subgraph "MediatR"
        SENDER[ISender<br/>Send commands/queries]
    end

    AC --> SENDER
    ACC --> SENDER
    UC --> SENDER

    SENDER --> EXC

3.3 Application Layer Components (CQRS)

graph LR
    subgraph "Commands Write Operations"
        CMD1[CreateAnimalCommand<br/>+ Handler<br/>+ Validator]
        CMD2[CreateAccountCommand<br/>+ Handler<br/>+ Validator]
    end

    subgraph "Queries Read Operations"
        Q1[GetAnimalQuery<br/>+ Handler]
        Q2[GetAllAnimalsQuery<br/>+ Handler]
        Q3[GetAccountQuery<br/>+ Handler]
        Q4[GetUserQuery<br/>+ Handler]
    end

    subgraph "Cross-Cutting"
        VB[Validation Behaviour<br/>MediatR Pipeline<br/>Auto-validates before handlers]
        MAP[AutoMapper Profile<br/>Entity to DTO mapping]
    end

    subgraph "DTOs"
        DTO1[AnimalDto]
        DTO2[AccountDto]
        DTO3[UserDto]
        DTO4[WeightDto]
    end

    CMD1 --> VB
    CMD2 --> VB
    Q1 --> MAP
    Q2 --> MAP
    MAP --> DTO1
    MAP --> DTO2
    MAP --> DTO3

3.4 Domain Layer Components

graph TB
    subgraph "Entities Rich Domain Models"
        E1[Account<br/>+ Create<br/>+ AddUser<br/>+ RemoveUser]
        E2[Animal<br/>+ Create<br/>+ SetDateOfBirth<br/>+ SetCurrentWeight]
        E3[User<br/>+ Create<br/>+ SetEmailAddress]
    end

    subgraph "Value Objects Immutable"
        VO1[Email<br/>RFC 5322 validation]
        VO2[Name<br/>1-100 chars]
        VO3[Weight<br/>Kg/Lb/G conversion]
        VO4[Species]
        VO5[Breed]
        VO6[Age<br/>Calculated from DOB]
    end

    subgraph "Repository Interfaces"
        R1[IAnimalRepository]
        R2[IAccountRepository]
        R3[IUserRepository]
        R4[IUnitOfWork]
    end

    subgraph "Domain Events"
        DE1[AnimalCreatedEvent]
        DE2[AccountCreatedEvent]
        DE3[AccountUserAddedEvent]
    end

    subgraph "Common Types"
        CT1[Result&lt;T&gt;<br/>Success/Failure pattern]
        CT2[PetfolioError<br/>Code + Message]
        CT3[Entity Base Class]
    end

    E2 --> VO1
    E2 --> VO2
    E2 --> VO3
    E3 --> VO1
    E1 --> VO2
    E2 --> DE1
    E1 --> DE2

3.5 Infrastructure Layer Components

graph TB
    subgraph "Data Access"
        DBC[PetfolioDbContext<br/>DbSet&lt;Account&gt;<br/>DbSet&lt;User&gt;<br/>DbSet&lt;Animal&gt;<br/>SaveChangesAsync]
    end

    subgraph "EF Core Configurations"
        CFG1[AccountConfiguration<br/>Table mapping<br/>Relationships<br/>Indexes]
        CFG2[AnimalConfiguration<br/>Value object conversions<br/>Owned Weight records]
        CFG3[UserConfiguration<br/>Email index<br/>OAuth indexes]
    end

    subgraph "Repository Implementations"
        REPO1[AnimalRepository<br/>implements IAnimalRepository]
        REPO2[AccountRepository<br/>implements IAccountRepository]
        REPO3[UserRepository<br/>implements IUserRepository]
        REPO4[UnitOfWork<br/>Transaction coordination]
    end

    DBC --> CFG1
    DBC --> CFG2
    DBC --> CFG3

    REPO1 --> DBC
    REPO2 --> DBC
    REPO3 --> DBC
    REPO4 --> DBC

    DBC --> DB[(MySQL Database)]

Level 4: Code Diagram

Selected examples showing the internal structure of key components.

4.1 Animal Entity - Rich Domain Model

classDiagram
    class Entity {
        <<abstract>>
        +Guid Id
        +DateTime CreatedAtUtc
        +DateTime UpdatedAtUtc
        #List~IDomainEvent~ _domainEvents
        +IReadOnlyList~IDomainEvent~ DomainEvents
    }

    class Animal {
        -Guid _id
        -Guid _accountId
        -Name _name
        -Species _species
        -Breed _breed
        -Gender _gender
        -Age? _age
        -Description? _description
        -List~Weight~ _weightRecords
        +Guid Id
        +Guid AccountId
        +Name Name
        +Species Species
        +Breed Breed
        +Gender Gender
        +Age? Age
        +Description? Description
        +IReadOnlyList~Weight~ WeightRecords
        +Create(accountId, name, species, breed, gender, description)$ Animal
        +SetDateOfBirth(DateTime dateOfBirth) void
        +SetCurrentWeight(Weight weight) void
        -Animal()
    }

    class Name {
        <<sealed record>>
        +string Value
        +string Normalised
        +Create(string value)$ Name
        -Name()
        -Name(string value, string normalised)
    }

    class Weight {
        <<sealed record>>
        +double Value
        +UnitOfWeight Unit
        +DateTime RecordedOn
        +Create(double value, UnitOfWeight unit, DateTime recordedOn)$ Weight
        +ConvertTo(UnitOfWeight targetUnit) Weight
        -Weight()
    }

    class Species {
        <<sealed record>>
        +string Value
        +string Normalised
        +Create(string value)$ Species
    }

    class Gender {
        <<enumeration>>
        Male
        Female
        Unknown
    }

    Entity <|-- Animal
    Animal "1" *-- "1" Name
    Animal "1" *-- "1" Species
    Animal "1" *-- "0..*" Weight
    Animal "1" *-- "1" Gender

4.2 Value Object Pattern - Email

classDiagram
    class Email {
        <<sealed record>>
        +string Value
        +string Normalised
        +Create(string value)$ Email
        -Email()
        -Email(string value, string normalised)
        -ValidateEmail(string email)$ void
    }

    note for Email "Immutable value object\n- Private parameterless constructor for EF Core\n- Create() factory validates RFC 5322\n- Max length 254 chars\n- Normalises to lowercase\n- Throws ArgumentException if invalid"

4.3 CQRS Pattern - Command and Handler

classDiagram
    class ICommand~TResponse~ {
        <<interface>>
    }

    class ICommandHandler~TCommand, TResponse~ {
        <<interface>>
        +Handle(TCommand command, CancellationToken ct) Task~Result~TResponse~~
    }

    class CreateAnimalCommand {
        +Guid AccountId
        +string Name
        +string Species
        +string Breed
        +string Gender
        +string? Description
    }

    class CreateAnimalCommandHandler {
        -IAnimalRepository _animalRepository
        -IUnitOfWork _unitOfWork
        +CreateAnimalCommandHandler(repository, unitOfWork)
        +Handle(CreateAnimalCommand, CancellationToken) Task~Result~Guid~~
    }

    class CreateAnimalCommandValidator {
        +CreateAnimalCommandValidator()
        +RuleFor(x => x.Name)
        +RuleFor(x => x.Species)
        +RuleFor(x => x.AccountId)
    }

    ICommand~Guid~ <|.. CreateAnimalCommand
    ICommandHandler~CreateAnimalCommand, Guid~ <|.. CreateAnimalCommandHandler
    CreateAnimalCommandHandler ..> CreateAnimalCommand : handles

4.4 Repository Pattern

classDiagram
    class IAnimalRepository {
        <<interface>>
        +GetByIdAsync(Guid id, CancellationToken ct) Task~Animal?~
        +GetAllAsync(CancellationToken ct) Task~List~Animal~~
        +CreateAsync(Animal animal, CancellationToken ct) Task
    }

    class AnimalRepository {
        -PetfolioDbContext _context
        +AnimalRepository(PetfolioDbContext context)
        +GetByIdAsync(Guid id, CancellationToken ct) Task~Animal?~
        +GetAllAsync(CancellationToken ct) Task~List~Animal~~
        +CreateAsync(Animal animal, CancellationToken ct) Task
    }

    class IUnitOfWork {
        <<interface>>
        +SaveChangesAsync(CancellationToken ct) Task~int~
    }

    class UnitOfWork {
        -PetfolioDbContext _context
        +UnitOfWork(PetfolioDbContext context)
        +SaveChangesAsync(CancellationToken ct) Task~int~
    }

    IAnimalRepository <|.. AnimalRepository
    IUnitOfWork <|.. UnitOfWork

4.5 Result Pattern

classDiagram
    class Result~T~ {
        <<sealed>>
        +bool IsSuccess
        +bool IsFailure
        +T Value
        +PetfolioError Error
        +Success(T value)$ Result~T~
        +Failure(PetfolioError error)$ Result~T~
        -Result(T value, bool isSuccess, PetfolioError error)
    }

    class PetfolioError {
        +string Code
        +string Message
        +PetfolioError(string code, string message)
        +None$ PetfolioError
    }

    Result~T~ "1" *-- "1" PetfolioError

    note for Result~T~ "Type-safe error handling\n- Success: Contains value\n- Failure: Contains error\n- Used at Application layer only\n- Domain throws exceptions"

CQRS Request Flow

Complete Request Pipeline for CreateAnimalCommand

sequenceDiagram
    actor User
    participant Controller as AnimalsController
    participant MediatR as MediatR Pipeline
    participant Validator as ValidationBehaviour
    participant FluentVal as CreateAnimalCommandValidator
    participant Handler as CreateAnimalCommandHandler
    participant Domain as Animal.Create()
    participant Repo as IAnimalRepository
    participant UoW as IUnitOfWork
    participant DB as MySQL Database

    User->>Controller: POST /api/Animals/Create<br/>{name, species, breed, ...}

    Controller->>MediatR: _sender.Send(CreateAnimalCommand)

    rect rgb(255, 245, 230)
        Note over MediatR,FluentVal: Step 1: Automatic Validation
        MediatR->>Validator: Execute ValidationBehaviour
        Validator->>FluentVal: Validate(command)

        alt Validation Fails
            FluentVal-->>Validator: Validation errors
            Validator-->>Controller: throw ValidationException
            Controller-->>User: 400 Bad Request<br/>{errors: [...]}
        else Validation Passes
            FluentVal-->>Validator: Valid
        end
    end

    rect rgb(230, 245, 255)
        Note over MediatR,DB: Step 2: Command Handler Execution
        MediatR->>Handler: Handle(CreateAnimalCommand)

        Handler->>Domain: Animal.Create(accountId, name, ...)

        alt Domain Validation Fails
            Domain-->>Handler: throw ArgumentException
            Handler->>Handler: Catch exception
            Handler-->>MediatR: Result.Failure(error)
        else Domain Validation Passes
            Domain->>Domain: Raise AnimalCreatedDomainEvent
            Domain-->>Handler: Return Animal entity

            Handler->>Repo: CreateAsync(animal)
            Repo->>Repo: _context.Animals.Add(animal)
            Repo-->>Handler: Task completed

            Handler->>UoW: SaveChangesAsync()
            UoW->>DB: INSERT INTO Animals...
            DB-->>UoW: Success
            UoW-->>Handler: Changes saved

            Handler-->>MediatR: Result.Success(animalId)
        end
    end

    MediatR-->>Controller: Result<Guid>

    alt Result.IsSuccess
        Controller-->>User: 201 Created<br/>{id: "guid", ...}
    else Result.IsFailure
        Controller-->>User: 400 Bad Request<br/>{error: {...}}
    end

Query Request Flow

sequenceDiagram
    actor User
    participant Controller as AnimalsController
    participant MediatR as MediatR Pipeline
    participant Handler as GetAnimalQueryHandler
    participant Repo as IAnimalRepository
    participant DB as MySQL Database
    participant Mapper as AutoMapper

    User->>Controller: GET /api/Animals/Get/{id}

    Controller->>MediatR: _sender.Send(GetAnimalQuery)

    MediatR->>Handler: Handle(GetAnimalQuery)

    Handler->>Repo: GetByIdAsync(id)
    Repo->>DB: SELECT * FROM Animals WHERE Id = @id

    alt Animal Not Found
        DB-->>Repo: null
        Repo-->>Handler: null
        Handler-->>MediatR: Result.Failure(AnimalErrors.NotFound)
        MediatR-->>Controller: Result<AnimalDto> (failure)
        Controller-->>User: 404 Not Found
    else Animal Found
        DB-->>Repo: Animal entity
        Repo-->>Handler: Animal entity

        Handler->>Mapper: Map<AnimalDto>(animal)
        Mapper-->>Handler: AnimalDto

        Handler-->>MediatR: Result.Success(animalDto)
        MediatR-->>Controller: Result<AnimalDto> (success)
        Controller-->>User: 200 OK<br/>{id, name, species, ...}
    end

Summary

C4 Architecture Overview

Level Focus Key Elements
Context System in environment Users (pet owners, vets), Petfolio Service, MySQL, OAuth (planned)
Container Applications & data stores .NET 9 API (4-layer architecture), MySQL 8.0 database
Component Major structural components Controllers, CQRS handlers, entities, repositories, EF Core
Code Class structure & patterns Rich domain models, value objects, Result pattern, MediatR pipeline

Key Architectural Patterns

Clean Architecture (Dependency Rule)

1
2
3
API Layer -> Application Layer -> Domain Layer <- Infrastructure Layer
                                     ^
                              (All depend on Domain)

Dependency Flow:

  • API -> Application + Infrastructure
  • Application -> Domain only
  • Infrastructure -> Domain only
  • Domain -> NOTHING (zero dependencies)

CQRS (Command Query Responsibility Segregation)

Commands (Write operations):

  • Create, Update, Delete operations
  • Return Result<T> (success/failure)
  • Include validation via FluentValidation
  • Modify state and persist changes

Queries (Read operations):

  • Read-only operations
  • Return Result<T> with DTOs
  • No state modification
  • Optimised for data retrieval

Validation Strategy (Two-Layer)

Layer 1: Domain Validation

  • Value objects: Create() factory methods throw ArgumentException
  • Entities: Business rule validation throws exceptions
  • Enforces invariants and business rules
  • Example: Email must be valid RFC 5322, Name 1-100 chars

Layer 2: Application Validation

  • FluentValidation validators for DTOs/Commands
  • Runs automatically via ValidationBehaviour MediatR pipeline
  • Executes BEFORE handler (handler never runs if validation fails)
  • Example: Required fields, format checks, business constraints

Result Pattern

Purpose: Type-safe error handling without exceptions at application layer

Usage:

  • Application layer returns Result<T> (Success or Failure)
  • Domain layer throws exceptions (converted by handlers)
  • Controllers convert Result<T> to HTTP status codes

Benefits:

  • Explicit error handling
  • No silent failures
  • Clear success/failure states
  • Type-safe error information

Technology Stack

Layer Technologies
API ASP.NET Core 9, Swashbuckle (Swagger)
Application MediatR 13.1, FluentValidation 12.1, AutoMapper 15.0
Domain Pure C# (no dependencies)
Infrastructure EF Core 9.0, Pomelo MySQL Provider 9.0
Testing xUnit, Shouldly, NSubstitute, Bogus, Testcontainers

Database Schema

Relationships:

1
2
3
Account (1) --< Users (N)
   |
   +--< Animals (N) --< Weight (N)

Key Features:

  • GUID primary keys
  • RESTRICT delete on Account relationships (prevent orphans)
  • CASCADE delete on Weight (owned by Animal)
  • Indexes on foreign keys and email
  • Check constraints on Weight values
  • Unique constraint on (AuthProvider, AuthProviderUserId)

API Endpoints

Resource Method Endpoint Description
Animals POST /api/Animals/Create Create new animal
Animals GET /api/Animals/Get/{id} Get animal by ID
Animals GET /api/Animals/GetAll List all animals
Accounts POST /api/Accounts/Create Create new account
Accounts GET /api/Accounts/Get/{id} Get account by ID
Accounts GET /api/Accounts/GetAll List all accounts
Users GET /api/Users/Get/{id} Get user by ID
Users GET /api/Users/GetAll List all users

Base URL: https://localhost:7195 API Documentation: https://localhost:7195/swagger/index.html

Design Principles Applied

SOLID Principles

  • Single Responsibility: Each class has one reason to change
  • Open/Closed: Extensible via interfaces, closed for modification
  • Liskov Substitution: Interfaces and abstract classes enable substitution
  • Interface Segregation: Focused repository interfaces
  • Dependency Inversion: All layers depend on Domain abstractions

Domain-Driven Design

  • Rich domain models with behaviour
  • Immutable value objects
  • Aggregate roots (Account, Animal, User)
  • Domain events for cross-aggregate communication
  • Ubiquitous language in code

Clean Code

  • One class per file
  • Feature folders organisation
  • Consistent naming conventions
  • English UK spelling throughout
  • Comprehensive testing (unit + integration)