A .NET 8 Web API application that fetches and stores blockchain data from multiple cryptocurrency networks using the BlockCypher API.
This application fetches real-time blockchain data from ETH, Dash, BTC, and LTC networks, stores it in a SQL Server database with timestamps, and provides API endpoints to query the historical data.
The application integrates with the BlockCypher API to fetch blockchain data from the following endpoints:
| Blockchain | Endpoint |
|---|---|
| Ethereum (Main) | https://api.blockcypher.com/v1/eth/main |
| Dash (Main) | https://api.blockcypher.com/v1/dash/main |
| Bitcoin (Main) | https://api.blockcypher.com/v1/btc/main |
| Bitcoin (Testnet3) | https://api.blockcypher.com/v1/btc/test3 |
| Litecoin (Main) | https://api.blockcypher.com/v1/ltc/main |
- .NET 8 Core application based on Clean Architecture
- SOLID principles throughout the codebase
- Dependency Injection for loose coupling
- Repository pattern for data access
- Structured logging with Microsoft.Extensions.Logging
- RESTful API endpoints exposed via Swagger/OpenAPI
- Endpoints to fetch and store blockchain data from all supported networks
- History endpoints showing stored blockchain data ordered by
CreatedAt(descending) - All HTTP requests documented in Swagger UI
- SQL Server database for persistent storage
- Each blockchain record includes a
CreatedAttimestamp indicating when the data was fetched - Historical data queryable with descending order by creation time
- Health Checks endpoint (
/health) for monitoring application status - CORS Policy configured for cross-origin requests
- Model mapping using extension methods (
ToDto()) - Comprehensive error handling and logging
- camelCase property naming for JSON responses (REST API standard)
- Null value omission - null properties excluded from responses
- Enum as strings - enums serialized as readable strings instead of numbers
- FluentValidation for declarative validation rules
- MediatR Pipeline Behavior for automatic validation before handler execution
- Structured error responses following RFC 7807 Problem Details format
- Validation rules for symbols, networks, and query parameters
All Docker configuration files are in the Docker/ folder.
Navigate to the Docker folder and run:
cd Docker
docker-compose up -dThis will setup and run both the SQL Server and .NET project. The init db script will run automatically.
Swagger UI: http://localhost:5007/swagger
Health Check: http://localhost:5007/health
navigate to the Docker folder by running the command below in the terminal/powershell or use File Explorer: then running the docker-compose command in the terminal/powershell:
cd Docker
docker-compose -f docker-compose.sqlserver.yml up -dOption A: Visual Studio - Open solution, select HTTPS configuration, press Run
Option B: Command line:
dotnet run --project BlockchainAPIOption C: Docker:
cd Docker
docker-compose -f docker-compose.api.yml up -dBlockchainAPI/
├── BlockchainAPI/ # API Layer
│ ├── Controllers/ # API Controllers
│ ├── Middleware/ # Custom middleware (ValidationExceptionMiddleware)
│ └── Program.cs # Application entry point
├── BlockchainAPI.Application/ # Application Layer
│ ├── Behaviors/ # MediatR pipeline behaviors (ValidationBehavior)
│ ├── CQRS/ # Commands and Queries with handlers
│ ├── Extensions/ # Extension methods (BlockchainDataExtensions)
│ ├── Infrastructure/ # Interfaces
│ ├── Models/ # DTOs
│ └── Validators/ # FluentValidation validators
├── BlockchainAPI.Domain/ # Domain Layer (Entities)
├── BlockchainAPI.Infrastructure/ # Infrastructure Layer (Services, Repositories, DbContext)
├── BlockchainAPI.Tests/ # Test Project (Unit, Integration, Functional)
│ ├── Unit/ # Unit tests
│ │ ├── Behaviors/ # ValidationBehavior tests
│ │ ├── Handlers/ # CQRS handler tests
│ │ ├── Middleware/ # GlobalExceptionMiddleware tests
│ │ ├── Repositories/ # Repository tests
│ │ ├── Services/ # Service tests
│ │ └── Validators/ # Validator tests
│ ├── Integration/ # Integration tests
│ └── Functional/ # End-to-end tests
└── Docker/ # Docker configuration files
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/blockchain/{symbol} |
Get latest blockchain data for a symbol |
GET |
/api/blockchain/{symbol}/history |
Get historical data (ordered by CreatedAt desc) |
POST |
/api/blockchain/{symbol}/fetch |
Fetch and store latest data from BlockCypher |
GET |
/api/blockchain/all |
Get all supported blockchains' latest data |
GET |
/health |
Health check endpoint |
Supported symbols: eth, dash, btc, btc-test, ltc
| File | Purpose |
|---|---|
docker-compose.yml |
Full stack (SQL Server + API) |
docker-compose.sqlserver.yml |
SQL Server database only |
docker-compose.api.yml |
.NET API only |
- Database:
mcr.microsoft.com/mssql/server:2022-latest - API:
mcr.microsoft.com/dotnet/aspnet:8.0
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=BlockchainDb;..."
},
"BlockCypher": {
"BaseUrl": "https://api.blockcypher.com/v1",
"Endpoints": {
"ETH": "/eth/main",
"DASH": "/dash/main",
"BTC": "/btc/main",
"BTC_TEST": "/btc/test3",
"LTC": "/ltc/main"
}
}
}| Column | Type | Description |
|---|---|---|
Id |
int | Primary key |
Symbol |
nvarchar(20) | Blockchain symbol (ETH, BTC, etc.) |
Network |
nvarchar(20) | Network type (main, test3) |
Name |
nvarchar(100) | Blockchain name |
Height |
bigint | Current block height |
Hash |
nvarchar(100) | Latest block hash |
Time |
datetime | Block time from API |
LatestUrl |
nvarchar(500) | URL to latest block |
PreviousHash |
nvarchar(100) | Previous block hash |
PreviousUrl |
nvarchar(500) | URL to previous block |
PeerCount |
int | Number of peers |
UnconfirmedCount |
int | Unconfirmed transactions |
HighFeePerKb |
bigint | High fee per KB |
MediumFeePerKb |
bigint | Medium fee per KB |
LowFeePerKb |
bigint | Low fee per KB |
LastForkHeight |
bigint | Last fork height |
LastForkHash |
nvarchar(100) | Last fork hash |
CreatedAt |
datetime | Timestamp when data was fetched |
The project includes comprehensive test coverage with 119 tests across three testing levels: Unit Tests, Integration Tests and Functional Tests.
# Run all tests
dotnet test
# Run with verbose output
dotnet test --verbosity normal
# Run specific test category
dotnet test --filter "FullyQualifiedName~Unit"
dotnet test --filter "FullyQualifiedName~Integration"
dotnet test --filter "FullyQualifiedName~Functional"| Package | Purpose |
|---|---|
| NUnit | Test framework |
| Moq | Mocking framework |
| FluentAssertions | Assertion library |
| Microsoft.AspNetCore.Mvc.Testing | Integration testing |
| Microsoft.EntityFrameworkCore.InMemory | In-memory database for tests |
| Moq.Contrib.HttpClient | HTTP client mocking |
Unit tests verify individual components in isolation using mocks for dependencies.
Tests for the external API service that fetches blockchain data:
| Test | Description |
|---|---|
GetSupportedBlockchains_ShouldReturnAllSupportedBlockchains |
Verifies all 5 blockchains are returned |
FetchBlockchainDataAsync_WithValidResponse_ShouldReturnMappedData |
Tests successful API response mapping |
FetchBlockchainDataAsync_WithUnsupportedBlockchain_ShouldReturnNull |
Tests handling of invalid blockchain requests |
FetchBlockchainDataAsync_WithCaseInsensitiveSymbol_ShouldWork |
Verifies case-insensitive symbol handling |
FetchBlockchainDataAsync_WithHttpError_ShouldReturnNull |
Tests graceful HTTP error handling |
FetchBlockchainDataAsync_WithInvalidJson_ShouldReturnNull |
Tests invalid JSON response handling |
FetchAllBlockchainsAsync_ShouldFetchAllSupportedBlockchains |
Tests bulk fetch operation |
FetchAllBlockchainsAsync_WithPartialFailure_ShouldReturnOnlySuccessful |
Tests partial failure scenarios |
Tests for database operations using EF Core InMemory provider:
| Test | Description |
|---|---|
AddAsync_ShouldAddBlockchainData |
Verifies data insertion |
AddAsync_ShouldAllowMultipleEntriesForSameBlockchain |
Tests multiple entries per blockchain |
GetLatestAsync_WithExistingData_ShouldReturnMostRecent |
Tests latest record retrieval |
GetLatestAsync_WithNoData_ShouldReturnNull |
Tests empty database handling |
GetLatestAsync_ShouldFilterBySymbolAndNetwork |
Tests filtering logic |
GetHistoryAsync_ShouldReturnOrderedByCreatedAtDescending |
Verifies ordering |
GetHistoryAsync_ShouldRespectLimit |
Tests pagination limit |
GetAllLatestAsync_ShouldReturnLatestForEachBlockchain |
Tests grouped latest retrieval |
GetAllLatestAsync_ShouldDistinguishBetweenNetworks |
Tests network differentiation |
Tests for MediatR command and query handlers:
FetchBlockchainDataCommandHandler:
- Validates fetch and store workflow
- Tests null response handling
- Verifies symbol/network normalization
- Tests DTO mapping
FetchAllBlockchainsCommandHandler:
- Tests bulk fetch and store
- Verifies partial success handling
GetLatestBlockchainQueryHandler:
- Tests data retrieval and mapping
- Verifies null handling
GetBlockchainHistoryQueryHandler:
- Tests history retrieval
- Verifies limit parameter passing
Tests for FluentValidation validators ensuring request data is valid:
FetchBlockchainDataCommandValidator (10 tests):
| Test | Description |
|---|---|
Validate_WithValidSymbolAndNetwork_ShouldPass |
Tests valid combinations (BTC/main, ETH/main, etc.) |
Validate_WithEmptySymbol_ShouldFail |
Verifies symbol is required |
Validate_WithInvalidSymbol_ShouldFail |
Tests unsupported symbols (DOGE, XRP, etc.) |
Validate_WithEmptyNetwork_ShouldFail |
Verifies network is required |
Validate_WithInvalidNetwork_ShouldFail |
Tests unsupported networks |
Validate_WithTest3NetworkForNonBtc_ShouldFail |
Only BTC supports test3 network |
GetLatestBlockchainQueryValidator (10 tests):
- Same validation rules as FetchBlockchainDataCommand
- Tests symbol, network, and combination validation
GetBlockchainHistoryQueryValidator (14 tests):
| Test | Description |
|---|---|
Validate_WithValidInput_ShouldPass |
Tests valid symbol, network, and limit combinations |
Validate_WithDefaultLimit_ShouldPass |
Default limit (100) is valid |
Validate_WithZeroLimit_ShouldFail |
Limit must be at least 1 |
Validate_WithNegativeLimit_ShouldFail |
Negative limits are invalid |
Validate_WithLimitExceedingMaximum_ShouldFail |
Limit cannot exceed 1000 |
Validate_WithMultipleErrors_ShouldReturnAllErrors |
Multiple validation errors aggregated |
Tests for the global exception handling middleware (17 tests):
| Test | Description |
|---|---|
InvokeAsync_WithArgumentNullException_Returns400BadRequest |
Maps ArgumentNullException to 400 |
InvokeAsync_WithUnauthorizedAccessException_Returns401Unauthorized |
Maps UnauthorizedAccessException to 401 |
InvokeAsync_WithKeyNotFoundException_Returns404NotFound |
Maps KeyNotFoundException to 404 |
InvokeAsync_WithInvalidOperationException_Returns409Conflict |
Maps InvalidOperationException to 409 |
InvokeAsync_WithTimeoutException_Returns504GatewayTimeout |
Maps TimeoutException to 504 |
InvokeAsync_WithHttpRequestException_Returns502BadGateway |
Maps HttpRequestException to 502 |
InvokeAsync_WithGenericException_Returns500InternalServerError |
Maps unknown exceptions to 500 |
InvokeAsync_InDevelopment_ShouldIncludeStackTrace |
Stack traces shown in development |
InvokeAsync_InProduction_ShouldNotIncludeStackTrace |
Stack traces hidden in production |
InvokeAsync_ShouldReturnProblemDetailsFormat |
Returns RFC 7807 format |
InvokeAsync_ShouldIncludeRequestPath |
Includes request path in response |
InvokeAsync_ShouldLogError |
Logs all exceptions |
Tests for the MediatR pipeline behavior:
| Test | Description |
|---|---|
Constructor_WithValidators_ShouldNotThrow |
Behavior accepts validators |
Constructor_WithEmptyValidators_ShouldNotThrow |
Behavior handles no validators |
ValidationBehavior_ShouldBeRegisteredAsOpenGeneric |
Works with different request types |
Integration tests verify the API endpoints work correctly with the full middleware pipeline using WebApplicationFactory and an in-memory database.
Location: Integration/BlockchainControllerIntegrationTests.cs
| Test | Description |
|---|---|
GetSupportedBlockchains_ShouldReturnAllSupported |
Tests GET /api/blockchain/supported |
GetLatest_WithExistingData_ShouldReturnData |
Tests GET /api/blockchain/{symbol}/{network} with data |
GetLatest_WithNoData_ShouldReturn404 |
Tests 404 response when no data exists |
GetLatest_ShouldBeCaseInsensitive |
Verifies case-insensitive URL handling |
GetHistory_ShouldReturnOrderedData |
Tests history endpoint ordering |
GetHistory_ShouldRespectLimit |
Tests history pagination |
GetHistory_WithNoData_ShouldReturnEmptyList |
Tests empty history response |
GetAllLatest_ShouldReturnLatestForEachBlockchain |
Tests all latest endpoint |
FetchAndStore_WithValidData_ShouldReturnStoredData |
Tests POST fetch endpoint |
FetchAndStore_WhenServiceFails_ShouldReturn503 |
Tests service unavailable response |
HealthCheck_ShouldReturnHealthy |
Tests /health endpoint |
End-to-end tests that simulate complete user workflows through the API.
Location: Functional/BlockchainApiEndToEndTests.cs
| Test | Description |
|---|---|
CompleteUserWorkflow_FetchAndViewBlockchainData |
Full workflow: check supported → fetch → retrieve → fetch again → view history |
BulkFetchWorkflow_FetchAllAndViewAll |
Tests fetching all blockchains at once |
DataConsistencyWorkflow_MultipleFetches |
Verifies data integrity across multiple fetches |
ErrorHandlingWorkflow_ServiceFailure |
Tests graceful error handling when external API fails |
PaginationWorkflow_HistoryWithLimit |
Tests history pagination with various limits |
MultiNetworkWorkflow_SameSymbolDifferentNetworks |
Tests BTC main vs BTC test3 differentiation |
The integration and functional tests use:
- In-Memory Database: Each test class gets an isolated database instance using
InMemoryDatabaseRoot - Mocked External Services:
IBlockCypherServiceis mocked to avoid real API calls - Test Environment: Tests run in "Testing" environment to skip migrations
- HTTPS Handling: Tests use HTTPS base address to handle redirection properly
// Example test setup
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
// Replace DbContext with in-memory version
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase(databaseName, databaseRoot));
// Replace external service with mock
services.AddSingleton(mockBlockCypherService.Object);
});
});The API uses custom JSON serialization settings for consistent, REST-compliant responses.
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
// Use camelCase for property names (REST API standard)
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
// Don't include null values in response
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
// Handle enums as strings instead of numbers
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});Before (default serialization):
{
"Symbol": "BTC",
"Network": "main",
"LastForkHash": null,
"LastForkHeight": null
}After (custom serialization):
{
"symbol": "BTC",
"network": "main"
}The API uses FluentValidation with MediatR pipeline behaviors for automatic request validation.
Request → Controller → MediatR → ValidationBehavior → Handler
↓
If invalid:
ValidationException thrown
↓
ValidationExceptionMiddleware
↓
400 Bad Request response
| Parameter | Rules |
|---|---|
Symbol |
Required, must be: ETH, DASH, BTC, or LTC |
Network |
Required, must be: main or test3 |
Limit |
Must be between 1 and 1000 |
| Combination | test3 network only valid for BTC |
Request: GET /api/blockchain/INVALID/main
Response (400 Bad Request):
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Symbol": ["Invalid symbol. Supported symbols: ETH, DASH, BTC, LTC"]
}
}| File | Purpose |
|---|---|
Application/Behaviors/ValidationBehavior.cs |
MediatR pipeline that runs validators |
Application/Validators/*Validator.cs |
FluentValidation rule definitions |
BlockchainAPI/Middleware/ValidationExceptionMiddleware.cs |
Converts exceptions to HTTP responses |
// Create validator in Application/Validators/
public class MyCommandValidator : AbstractValidator<MyCommand>
{
public MyCommandValidator()
{
RuleFor(x => x.Property)
.NotEmpty()
.WithMessage("Property is required.");
}
}
// Validators are auto-registered via:
services.AddValidatorsFromAssembly(assembly);The application implements a comprehensive multi-layered error handling strategy.
Request → GlobalExceptionMiddleware → ValidationExceptionMiddleware → Controller → Handler
↓ (catches all) ↓ (catches validation) ↓
500/4xx responses 400 Bad Request 404/503 responses
Catches all unhandled exceptions and returns consistent RFC 7807 Problem Details responses.
Features:
- Maps exception types to appropriate HTTP status codes
- Includes correlation ID for error tracking
- Hides sensitive details (stack traces) in production
- Logs all errors with context information
Exception Mapping:
| Exception Type | HTTP Status | Description |
|---|---|---|
ArgumentNullException |
400 Bad Request | Required parameter was null |
ArgumentException |
400 Bad Request | Invalid parameter |
UnauthorizedAccessException |
401 Unauthorized | Access denied |
KeyNotFoundException |
404 Not Found | Resource not found |
InvalidOperationException |
409 Conflict | Operation conflict |
TimeoutException |
504 Gateway Timeout | Operation timed out |
HttpRequestException |
502 Bad Gateway | External service error |
OperationCanceledException |
499 Client Closed | Request cancelled |
| All other exceptions | 500 Internal Server Error | Unexpected error |
Example Error Response:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title": "An unexpected error occurred.",
"status": 500,
"detail": "An error occurred while processing your request. Please try again later.",
"instance": "/api/blockchain/btc/main",
"correlationId": "0HN8J4K5L6M7N8O9"
}Handles FluentValidation exceptions and returns structured validation error responses.
Example Validation Error:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Symbol": ["Invalid symbol. Supported symbols: ETH, DASH, BTC, LTC"],
"Limit": ["Limit must be between 1 and 1000"]
}
}BlockCypherService: Gracefully handles external API failures:
try
{
var response = await _httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
// Process response...
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error fetching blockchain data");
return null; // Allows caller to handle gracefully
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON parsing error");
return null;
}Controllers return appropriate HTTP status codes for business logic scenarios:
| Scenario | Status Code | Response |
|---|---|---|
| Data not found | 404 Not Found | Helpful message with suggestion |
| External service failure | 503 Service Unavailable | Error message |
| Success | 200 OK | Requested data |
HTTP client resilience for transient failures:
- Retry: 3 attempts with exponential backoff
- Circuit Breaker: Opens after 5 failures, 30-second break
- Timeout: 30-second per-request timeout
| File | Purpose |
|---|---|
Middleware/GlobalExceptionMiddleware.cs |
Catches all unhandled exceptions |
Middleware/ValidationExceptionMiddleware.cs |
Handles FluentValidation errors |
Infrastructure/ExternalServices/BlockCypherService.cs |
Service-level error handling |
Controllers/BlockchainController.cs |
Business logic error responses |
The application implements several performance optimizations following .NET best practices.
AsNoTracking is used for all read-only queries to reduce memory usage and improve query performance:
// Repository read queries don't track entities
var latest = await _context.BlockchainData
.AsNoTracking()
.Where(b => b.Symbol == symbol && b.Network == network)
.OrderByDescending(b => b.CreatedAt)
.FirstOrDefaultAsync(cancellationToken);Benefits:
- No change tracking overhead
- Reduced memory consumption
- Faster query execution for read-only scenarios
The repository implements a cache-aside pattern using IMemoryCache for frequently accessed data:
// Cache configuration
private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(30);
private const string LatestCacheKeyPrefix = "blockchain_latest_";
private const string AllLatestCacheKey = "blockchain_all_latest";
// Cache-aside pattern
if (_cache.TryGetValue(cacheKey, out BlockchainData? cachedData))
{
return cachedData; // Cache hit
}
var data = await FetchFromDatabase();
_cache.Set(cacheKey, data, CacheDuration);
return data;Caching Strategy:
| Query | Cache Key | TTL |
|---|---|---|
| GetLatestAsync | blockchain_latest_{symbol}_{network} |
30 seconds |
| GetAllLatestAsync | blockchain_all_latest |
30 seconds |
| GetHistoryAsync | Not cached (variable results) | N/A |
Cache Invalidation:
- Cache is automatically invalidated when new data is added via
AddAsync - Prevents stale data after write operations
The application uses Polly for resilient HTTP communication with the BlockCypher API:
// Retry with exponential backoff
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); // 2s, 4s, 8s
// Circuit breaker - stops calls after 5 failures
var circuitBreakerPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30));
// Request timeout
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));Policy Stack:
- Retry Policy: 3 retries with exponential backoff (2s → 4s → 8s)
- Circuit Breaker: Opens after 5 consecutive failures, stays open for 30 seconds
- Timeout: 30-second timeout per request
The application uses async/await throughout and applies parallel execution where appropriate.
Parallel External API Calls:
// BlockCypherService.cs - Fetch all blockchains in parallel
public async Task<IEnumerable<BlockchainData>> FetchAllBlockchainsAsync(CancellationToken cancellationToken)
{
var tasks = SupportedBlockchains.Select(b =>
FetchBlockchainDataAsync(b.Symbol, b.Network, cancellationToken));
var results = await Task.WhenAll(tasks); // All 5 API calls run concurrently
return results.Where(r => r != null).Cast<BlockchainData>();
}Parallel Validation:
// ValidationBehavior.cs - Run all validators concurrently
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));Pattern Usage Summary:
| Pattern | Where Used | Why |
|---|---|---|
Task.WhenAll |
External API calls | I/O-bound, independent operations |
Task.WhenAll |
FluentValidation | Multiple validators can run concurrently |
async/await |
All layers | Non-blocking I/O, thread pool efficiency |
| Sequential DB writes | Repository AddAsync |
EF Core DbContext is not thread-safe |
Design Decisions:
- Parallel API calls: External HTTP requests to BlockCypher run concurrently since they're independent I/O-bound operations
- Sequential DB writes: Database operations use sequential execution because EF Core's
DbContextis not thread-safe for concurrent operations - CancellationToken propagation: All async methods accept and propagate
CancellationTokenfor proper cancellation support
| Feature | Implementation | Benefit |
|---|---|---|
| Stateless API | No session state | Horizontal scaling |
| Connection pooling | EF Core default | Efficient DB connections |
| Async/await | Throughout codebase | Thread efficiency |
| Dependency injection | Scoped services | Request isolation |
| CQRS pattern | Separate read/write | Optimized query paths |