System Design

πŸ“Š Platform Overview

Sequence Diagram

🧠 High-Level Architecture (Backend)

The TrueTest backend is architected following the principles of Onion Architecture, emphasizing a clear separation of concerns. This design promotes enhanced testability, maintainability, and scalability by ensuring that core business logic is independent of infrastructure details. The architecture is organized into distinct layers, with dependencies directed inwards towards the Domain layer. We adopted the Onion Architecture template from the following open-source repository: Amitpnk/Onion-architecture-ASP.NET-Core, which served as the foundational structure for our backend solution.

Onion Architecture

πŸ—ƒοΈ Directory Structure

β”Œβ”€β”€ πŸ“‚ .github
β”‚   └── πŸ“‚ workflows
β”œβ”€β”€ πŸ“‚ src
β”‚   β”œβ”€β”€ πŸ“‚ api
β”‚   β”‚   β”œβ”€β”€ πŸ“‚ OPS.Api
β”‚   β”‚   β”œβ”€β”€ πŸ“‚ OPS.Application
β”‚   β”‚   β”œβ”€β”€ πŸ“‚ OPS.Domain
β”‚   β”‚   β”œβ”€β”€ πŸ“‚ OPS.Persistence
β”‚   β”‚   β”œβ”€β”€ πŸ“‚ OPS.Infrastructure
β”‚   β”‚   β”œβ”€β”€ πŸ“„ Directory.Packages.props
β”‚   β”‚   └── πŸ“„ Dockerfile
β”‚   └── πŸ“‚ client
β”‚       └── πŸ“„ Next.js stuff
β”œβ”€β”€ πŸ“‚ test
β”‚   β”œβ”€β”€ πŸ“‚ OPS.Application.Tests.Unit
β”‚   └── πŸ“„ Directory.Packages.props
β”œβ”€β”€ πŸ“„ .dockerignore
β”œβ”€β”€ πŸ“„ .editorconfig
β”œβ”€β”€ πŸ“„ .gitignore
β”œβ”€β”€ πŸ“„ docker-compose.yml
└── πŸ“„ OPS.sln

πŸ›οΈ Architecture Layers

Domain Layer

This is the heart of the application, containing core business entities, rules, and interfaces. It's a pure layer, independent of any infrastructure concerns, focusing solely on the business model. It defines what the system is and how the business operates, without specifying how these rules are implemented.

OPS.Domain
β”œβ”€β”€ πŸ“‚ Constants
β”œβ”€β”€ πŸ“‚ Enums
β”œβ”€β”€ πŸ“‚ Entities
β”‚   └── πŸ“‚ Users
β”‚       β”œβ”€β”€ πŸ“„ Account.cs
β”‚       └── πŸ“„ Profile.cs
β”œβ”€β”€ πŸ“‚ Interfaces
└── πŸ“„ IUnitOfWork.cs

Persistence Layer

This layer is responsible for the concrete implementation of data access. It interacts with the SQL Server database using Entity Framework Core, implementing repositories and the Unit of Work pattern defined in the Domain Layer. Its primary concern is the storage and retrieval of application data.

OPS.Persistence
β”œβ”€β”€ πŸ“‚ Configurations
β”œβ”€β”€ πŸ“‚ Migrations
β”œβ”€β”€ πŸ“‚ Seeding
β”œβ”€β”€ πŸ“‚ Repositories
β”œβ”€β”€ πŸ“„ AppDbContext.cs
└── πŸ“„ UnitOfWork.cs

Application Layer

This layer implements the system's use cases and business workflows. It orchestrates logic by handling commands and queries, utilizing the Domain Layer for business rules, and interacting with the Persistence and Infrastructure Layers through defined interfaces. It often employs patterns like CQRS and includes DTOs and mappers for data transfer.

OPS.Application
β”œβ”€β”€ πŸ“‚ Behaviors
β”‚   └── πŸ“„ ValidationPipelineBehavior.cs
β”œβ”€β”€ πŸ“‚ Common
β”œβ”€β”€ πŸ“‚ Features
β”‚   └── πŸ“‚ Exams
β”‚       β”œβ”€β”€ πŸ“‚ Command
β”‚       β”‚   β”œβ”€β”€ πŸ“„ CreateExamCommand.cs
β”‚       β”‚   β”œβ”€β”€ πŸ“„ PublishExamCommand.cs
β”‚       └── πŸ“‚ Query
β”‚           └── πŸ“„ GetExamByIdQuery.cs
β”œβ”€β”€ πŸ“‚ Dtos
β”œβ”€β”€ πŸ“‚ Mappers
β”œβ”€β”€ πŸ“‚ Interfaces
└── πŸ“‚ Services

Infrastructure Layer

This layer provides implementations for cross-cutting concerns and external system integrations. This includes services for authentication, logging, email, cloud storage, external API interactions, etc. It handles the "how" of certain functionalities, abstracting away the specifics from the inner layers.

OPS.Infrastructure
β”œβ”€β”€ πŸ“‚ Auth
β”‚   β”œβ”€β”€ πŸ“‚ Jwt
β”‚   └── πŸ“‚ Permission
β”œβ”€β”€ πŸ“‚ BackgroundServices
β”œβ”€β”€ πŸ“‚ Database
β”œβ”€β”€ πŸ“‚ Cloud
β”‚   β”œβ”€β”€ πŸ“‚ Configuration
β”‚   └── πŸ“„ GoogleCloudService.cs
β”œβ”€β”€ πŸ“‚ Gemini
β”‚   β”œβ”€β”€ πŸ“‚ Refit
β”‚   └── πŸ“„ GeminiService.cs
β”œβ”€β”€ πŸ“‚ Email
└── πŸ“‚ Logging

Presentation Layer

This layer exposes the system's functionalities through Web APIs using ASP.NET Core. It handles HTTP requests, manages routing, performs request validation, and orchestrates business logic via MediatR. It also handles authentication, authorization, and formats responses for the client.

OPS.Api
β”œβ”€β”€ πŸ“‚ Common
β”‚   β”œβ”€β”€ πŸ“‚ ErrorResponses
β”‚   └── πŸ“„ BaseApiController.cs
β”œβ”€β”€ πŸ“‚ Controllers
β”œβ”€β”€ πŸ“‚ Middlewares
β”œβ”€β”€ πŸ“‚ Transformers
└── πŸ“‚ wwwroot

Test Layer

This layer contains unit tests focused on verifying the logic within the Application Layer. These tests are isolated, ensuring the correctness and reliability of business workflows.

OPS.Application.Tests.Unit
β”œβ”€β”€ πŸ“‚ Features
β”‚   └── πŸ“‚ Exams
β”‚       β”œβ”€β”€ πŸ“‚ Command
β”‚       β”‚   β”œβ”€β”€ πŸ“„ CreateExamCommandTests.cs
β”‚       β”‚   β”œβ”€β”€ πŸ“„ PublishExamCommandTests.cs
β”‚       └── πŸ“‚ Query
β”‚           └── πŸ“„ GetExamByIdQueryTests.cs
└── πŸ“‚ Services

⚑ Request Flow

🧩 Design Patterns Used

TrueTest leverages several design patterns to promote modularity, maintainability, and a clean architecture. These patterns contribute to the system's flexibility, testability, and overall robustness.

CQRS Pattern

The Command Query Responsibility Segregation (CQRS) pattern is employed to separate data modification (commands) from data retrieval (queries). This separation allows for optimized handling of each type of operation.

  • Command (Write) and Query (Read) Separation: Operations that change the system's state (e.g., creating an exam, submitting an answer) are handled by commands, while operations that retrieve data (e.g., fetching exam details, displaying results) are handled by queries.

  • MediatR Implementation: This pattern is implemented using the MediatR library, which facilitates the dispatching of commands and queries to their respective handlers.

Mediator Pattern

The Mediator pattern is implemented using the MediatR library.

  • Loosely Coupled Communication: MediatR acts as a central mediator, enabling loosely coupled communication between different components of the application. Components do not communicate directly with each other; instead, they send messages (commands, queries, or events) to the mediator, which then dispatches them to the appropriate handlers.

  • Command, Query, and Event Dispatching: MediatR is used throughout the application to send commands, queries, and domain events, promoting a decoupled and maintainable architecture.

Result Pattern

The Result Pattern is implemented using the ErrorOr package.

  • Unified Success/Failure Handling: The ErrorOr package provides a unified way to represent the outcome of an operation, whether it's a success or a failure. This approach avoids the use of exceptions for control flow and promotes a more functional style of error handling.

  • Clean Error Handling and Chaining: This pattern simplifies error handling, allows for chaining of operations, and improves code readability by making the success or failure of an operation explicit.

Repository Pattern

The Repository pattern is used to abstract data access logic, decoupling the application from the specific database implementation.

  • Abstraction of Data Access: Repositories provide an interface for interacting with the database, hiding the underlying data storage mechanism from the Application Layer. This abstraction allows for easier testing and potential changes to the database without affecting the core business logic.

  • Generic Repository: A generic repository class is used to provide common CRUD (Create, Read, Update, Delete) and other operations for entities, reducing code duplication. Specific repository interfaces (e.g., IUserRepository, IExamRepository) are defined for entities with custom data access requirements, inheriting from the generic repository.

Unit of Work Pattern

The Unit of Work pattern is implemented to manage database transactions and ensure data consistency.

  • Transaction Management: The Unit of Work coordinates multiple repository operations within a single transaction, ensuring that either all operations succeed or none of them do.

  • Common Data Handling: The Unit of Work handles data-related concerns, such as soft deletes (marking records as deleted without physically removing them by setting the IsDeleted property to true for all the ISoftDeletable entities) and automatic management of audit properties, like UpdatedAt and DeletedAt.

πŸ—‚οΈ Entity Relationship Diagram (ERD)

πŸ›‘οΈ Validation, Error Handling, and Logging

Validation

Input validation is crucial for ensuring data integrity and preventing errors.

  • FluentValidation: The FluentValidation package is used to define validation rules.

  • MediatR Pipeline Behavior: Validation rules are applied using MediatR's pipeline behavior (ValidationPipelineBehavior.cs), which automatically validates incoming commands and queries before they reach their handlers. This approach centralizes validation logic and keeps handlers clean.

Error Handling

Robust error handling is essential for providing a reliable and user-friendly experience.

  • ErrorOr: The ErrorOr<T> type from the ErrorOr package is used to handle errors in a functional style, avoiding exceptions for normal control flow. This promotes predictable and maintainable error management.

  • Production Error Handling: In production, a middleware component (ExceptionHandleMiddleware.cs) is used to catch any unhandled exceptions and convert them into appropriate, standardized error responses, without exposing sensitive internal information to the client.

Logging

Comprehensive logging is implemented to aid in debugging, monitoring, and auditing.

  • Serilog: Serilog is used as the logging framework to record application events and errors.

  • Database Logging: All logs are persisted to the database, providing a centralized and persistent record of application activity (SerilogConfig.cs).

πŸ§ͺ Testing Strategy

TrueTest employs a comprehensive testing strategy to ensure the quality and reliability of the software.

  • Unit Testing Focus: The primary testing approach is unit testing, with a strong emphasis on testing the Application Layer, where the core business logic resides. This ensures that individual components function correctly in isolation.

  • Application Layer Testing: Unit tests are written for application layer handlers (commands and queries) to verify the correctness of business rules and workflows.

Tools Used

The following tools are used for unit testing:

  • xUnit: A popular framework for writing unit tests in .NET.

  • FluentAssertions: A library that provides a fluent interface for writing more expressive and readable assertions.

  • NSubstitute: A mocking framework used to create substitute objects (mocks) for dependencies, allowing for isolated testing of components.

🧱 Extensibility

TrueTest is designed with extensibility in mind, allowing for future enhancements and modifications without requiring significant changes to the core architecture.

  • Design for Extensibility: From the initial design phase, the system was built to accommodate future extensions.

  • Modular Architecture: The layered architecture and the use of design patterns like CQRS and the Repository pattern contribute to the system's modularity.

  • Ease of Extension: New features and functionalities can be added with minimal disruption to existing code.

  • Example Scenarios:

    • Easily add new question types, submission formats, or integrations with external services.

    • Replace infrastructure components (e.g., switch from SQL Server to PostgreSQL) with minimal code changes.

    • Add new roles or permission levels without requiring major architectural overhauls.

Last updated