ASP.NET Core & Enterprise Architecture

ASP.NET Core Enterprise Architecture:
Practical Patterns for Scalable .NET Applications

Practical architecture patterns for building scalable ASP.NET Core enterprise applications with clean boundaries, maintainable APIs, efficient data access and reliable integrations.

August 2026 • 14 min read • By Kshitij Mehta

ASP.NET Core enterprise architecture is about creating clear boundaries between business workflows, application services, data access, infrastructure and external integrations. As a .NET application grows, these boundaries help developers change one part of the system without creating unnecessary coupling throughout the rest of the codebase.

A business platform such as a shipping system is a useful real-world example. A single shipment workflow may involve an ASP.NET Core API, SQL Server, Dapper or Entity Framework, address and order processing, background jobs and external carrier APIs such as DHL, UPS, DPD and Asendia. A good enterprise architecture keeps those concerns separated while allowing them to work together as one business workflow.

This guide explains practical ASP.NET Core enterprise architecture patterns for C# and .NET applications, with examples covering layered architecture, dependency injection, application services, data access, API contracts, background processing, observability, testing and carrier API integration.

Key Takeaways
  • Keep business workflows separated from infrastructure details.
  • Use clear boundaries between APIs, application services and data access.
  • Isolate carrier-specific integrations behind stable contracts.
  • Choose Dapper, Entity Framework or ADO.NET according to the workload.
  • Keep background processing and external calls from unnecessarily blocking web requests.
  • Design observability, security and testability into the architecture.

1. What Enterprise Architecture Is Trying to Achieve

Enterprise architecture should make a large application easier to understand, test and change. The goal is not to maximize the number of projects, interfaces or frameworks. The goal is to create boundaries that correspond to real responsibilities in the application.

A practical enterprise .NET application can be viewed as:


Client / UI
    ↓
ASP.NET Core API
    ↓
Application Services
    ↓
Business Rules
    ↓
Data Access & Infrastructure
    ↓
SQL Server / External Services
                

This structure helps keep business workflows understandable even when the application integrates databases, third-party APIs and background processes.


2. Use Layers to Protect Business Workflows

Layered architecture remains useful when each layer has a clear responsibility. A common ASP.NET Core architecture separates presentation, application workflows, domain or business rules and infrastructure.


Presentation
    ↓
Application
    ↓
Domain
    ↓
Infrastructure
                

The application layer can coordinate a use case such as "create order" or "create shipment" without knowing whether shipment data is loaded through Dapper, Entity Framework or direct ADO.NET, and without knowing which carrier implementation will ultimately call DHL, UPS, DPD or Asendia.


3. Control Dependency Direction

One of the most useful principles in enterprise .NET architecture is controlling which part of the system is allowed to know about another part.


public interface IShippingCarrier
{
    Task<ShipmentResult> CreateShipmentAsync(
        ShipmentRequest request,
        CancellationToken cancellationToken);
}
                

The application workflow depends on this contract instead of depending directly on a DHL or UPS implementation. The infrastructure layer supplies the concrete implementation.

Not every class needs an interface. Abstraction is most valuable where it protects a meaningful boundary, especially around external systems and infrastructure that may change.


4. Use ASP.NET Core Dependency Injection for Composition

ASP.NET Core provides built-in dependency injection, making it possible to compose application services and infrastructure components without constructing those dependencies throughout the business code.


builder.Services.AddScoped<IShipmentService, ShipmentService>();
builder.Services.AddScoped<IShipmentRepository, ShipmentRepository>();
builder.Services.AddScoped<ICarrierResolver, CarrierResolver>();
                

The composition root chooses the concrete implementation while application workflows continue to work against stable contracts.


5. Keep Application Services Focused on Business Workflows

An application service should coordinate a use case rather than becoming a dumping ground for SQL, HTTP calls, mapping, logging and unrelated business rules.


public sealed class ShipmentService : IShipmentService
{
    private readonly IShipmentRepository _repository;
    private readonly ICarrierResolver _carrierResolver;

    public ShipmentService(
        IShipmentRepository repository,
        ICarrierResolver carrierResolver)
    {
        _repository = repository;
        _carrierResolver = carrierResolver;
    }

    public async Task<ShipmentResult> CreateAsync(
        ShipmentRequest request,
        CancellationToken cancellationToken)
    {
        var shipment =
            await _repository.GetShipmentDataAsync(
                request.OrderId,
                cancellationToken);

        var carrier =
            _carrierResolver.Resolve(
                request.CarrierCode);

        return await carrier.CreateShipmentAsync(
            shipment,
            cancellationToken);
    }
}
                

The resulting workflow is easier to read and the technical details can remain inside their appropriate components.


6. Choose the Right Data Access Strategy

Enterprise applications do not have to use one data-access technology for every operation. The right choice depends on the workload, SQL requirements and domain model.


ASP.NET Core Application
        │
        ├── Dapper
        │     └── Targeted SQL / reporting / read-heavy queries
        │
        ├── Entity Framework Core
        │     └── Entity-oriented workflows
        │
        └── ADO.NET
              └── Direct database operations / bulk processing
                

In a shipping platform, Dapper can be useful for targeted shipment queries, reports and operational screens while Entity Framework or ADO.NET can be used where their respective strengths fit the workload.


7. Use Repositories Where They Protect a Real Boundary

A repository can provide a useful application-facing boundary around persistence. It becomes less useful when every database operation is forced through a generic CRUD abstraction that hides important data-access capabilities.


public interface IShipmentRepository
{
    Task<ShipmentData> GetShipmentDataAsync(
        int orderId,
        CancellationToken cancellationToken);

    Task UpdateTrackingAsync(
        int orderId,
        string trackingNumber,
        CancellationToken cancellationToken);
}
                

Explicit business-oriented operations can communicate application intent more clearly than a generic repository containing dozens of unrelated methods.


8. Design Stable ASP.NET Core API Contracts

Enterprise APIs should expose contracts that represent application capabilities instead of leaking database entities directly to clients.


public sealed record CreateShipmentRequest(
    int OrderId,
    string CarrierCode);

public sealed record CreateShipmentResponse(
    string ShipmentReference,
    string TrackingNumber,
    string Status);
                

This allows internal tables and carrier models to evolve without forcing API consumers to change every time an internal implementation changes.


9. Separate External Models from Internal Models

DHL, UPS, DPD and Asendia can have different request, authentication and response structures. Passing those external models throughout the application creates unnecessary coupling.


Application Shipment
        ↓
Carrier Adapter
        ↓
DHL / UPS / DPD / Asendia Model
        ↓
External Carrier API
                

A carrier adapter translates the common application model into the structure required by the external service and maps the response back into a standard application result.


10. Build Carrier API Integrations Behind a Common Boundary

Multi-carrier shipping software benefits from a common carrier contract. The shipment workflow can resolve the required carrier without embedding carrier-specific code throughout the application.


public interface IShippingCarrier
{
    string Code { get; }

    Task<ShipmentResult> CreateShipmentAsync(
        ShipmentRequest request,
        CancellationToken cancellationToken);
}
                

Separate implementations can then handle DHL API Integration, UPS API Integration, DPD API Integration and Asendia API Integration while the common application workflow remains stable.


public sealed class CarrierResolver : ICarrierResolver
{
    private readonly IReadOnlyDictionary<string, IShippingCarrier> _carriers;

    public CarrierResolver(
        IEnumerable<IShippingCarrier> carriers)
    {
        _carriers = carriers.ToDictionary(
            carrier => carrier.Code,
            StringComparer.OrdinalIgnoreCase);
    }

    public IShippingCarrier Resolve(string code)
    {
        return _carriers[code];
    }
}
                

This design also makes it easier to add another carrier without duplicating the entire shipment workflow.


11. Use HttpClientFactory for External API Integrations

Carrier APIs are external dependencies. Keep HTTP transport and endpoint configuration inside the relevant integration layer rather than creating HTTP clients throughout business code.


builder.Services.AddHttpClient<DhlShippingCarrier>(
    client =>
    {
        client.BaseAddress =
            new Uri(configuration["Dhl:BaseUrl"]!);

        client.Timeout =
            TimeSpan.FromSeconds(30);
    });
                

The carrier component can then focus on authentication, request construction, response handling and mapping.


12. Use Background Processing for Long-Running Work

Tracking synchronization, bulk imports, scheduled cleanup and similar operations may not need to make a user's HTTP request wait for the entire workload.


public sealed class TrackingWorker : BackgroundService
{
    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await SynchronizeTrackingAsync(
                stoppingToken);

            await Task.Delay(
                TimeSpan.FromMinutes(5),
                stoppingToken);
        }
    }
}
                

Background workloads need explicit cancellation, failure handling, logging and operational visibility.


13. Keep Database Transactions Focused

Transactions should protect the set of database operations that must succeed or fail together. They should not normally remain open while the application waits on an external carrier API.


await using var transaction =
    await connection.BeginTransactionAsync(
        cancellationToken);

try
{
    await repository.InsertShipmentAsync(
        shipment,
        transaction);

    await repository.InsertPackagesAsync(
        packages,
        transaction);

    await transaction.CommitAsync(
        cancellationToken);
}
catch
{
    await transaction.RollbackAsync(
        cancellationToken);

    throw;
}
                

Separating SQL transaction boundaries from external calls reduces unnecessary coupling between database availability and carrier availability.


14. Put Validation at the Appropriate Boundary

Request-shape validation can happen at the API boundary, business validation should happen within the application workflow, and carrier-specific validation can remain inside a carrier adapter when the rule exists only because of that carrier.


if (request.OrderId <= 0)
{
    throw new ArgumentException(
        "A valid order identifier is required.",
        nameof(request.OrderId));
}
                

15. Keep Configuration Out of Business Logic

Database connection strings, carrier endpoints and other environment-specific settings should be supplied through configuration rather than embedded in application services.


public sealed class DhlOptions
{
    public string BaseUrl { get; set; } = string.Empty;

    public string ClientId { get; set; } = string.Empty;

    public string ClientSecret { get; set; } = string.Empty;
}
                

Typed configuration keeps infrastructure settings close to the component that consumes them without spreading raw configuration access throughout the business layer.


16. Treat Security as an Architectural Concern

Enterprise architecture should account for authentication, authorization, secrets management, validation, auditability and protection of sensitive information from the beginning.

  • Keep credentials out of source code and repositories.
  • Apply authorization at appropriate API and application boundaries.
  • Validate external input before processing business operations.
  • Do not expose internal database entities unnecessarily.
  • Avoid logging credentials or unnecessary customer data.

17. Build Observability into the Architecture

Enterprise applications need enough diagnostic context to determine where a failure occurred. In a shipment workflow, a useful correlation path can be:


Request ID
    ↓
Shipment Reference
    ↓
Application Workflow
    ↓
Database Operation
    ↓
Carrier Request
    ↓
Carrier Response
                

Logging should provide operation names, durations, statuses and correlation identifiers while avoiding credentials and unnecessary sensitive information.


18. Design Error Handling Around System Boundaries

A validation failure, SQL exception and carrier API rejection have different meanings and should not automatically share the same handling path.


try
{
    return await carrier.CreateShipmentAsync(
        request,
        cancellationToken);
}
catch (HttpRequestException ex)
{
    logger.LogError(
        ex,
        "Carrier communication failed for {ShipmentReference}",
        request.Reference);

    throw;
}
                

The application layer can translate infrastructure failures into consistent business outcomes while preserving useful diagnostic information for operations teams.


19. Design for Performance Without Premature Complexity

Enterprise architecture should make performance work possible without turning every component into a framework. Start with measurable bottlenecks.

  • Slow SQL queries
  • Excessive database round trips
  • Large result sets
  • Repeated external API calls
  • CPU-heavy synchronous processing
  • Unnecessary serialization and mapping

Dapper optimization, SQL tuning, caching and background processing should be introduced where measurement shows that they improve the workload.


20. Make Enterprise Architecture Testable

Clear boundaries make it possible to test application workflows without invoking every external system.


// Simplified test double example

var carrier = new TestShippingCarrier
{
    Result = new ShipmentResult
    {
        Success = true,
        TrackingNumber = "TEST-TRACKING"
    }
};
                

The business workflow can then be tested without making a real DHL API, UPS API, DPD API or Asendia API call.


21. Consider a Modular Monolith Before Microservices

Enterprise architecture does not automatically require microservices. A well-structured modular monolith can provide strong separation while keeping deployment and operational complexity manageable.


ASP.NET Core Application
        │
        ├── Shipments
        ├── Customers
        ├── Carrier Integrations
        ├── Tracking
        ├── Reporting
        └── Administration
                

Clear module boundaries can provide strong architectural separation before introducing the operational cost of multiple distributed services.


22. Applying Enterprise Architecture to ShippingPortal

ShippingPortal is a practical example of an integration-heavy enterprise application. Its workflows can involve shipment orders, sender and receiver addresses, carrier services, SQL Server processing, tracking and external carrier communication.

A practical architecture can therefore separate the flow:


Shipment Request
      ↓
ASP.NET Core / Application API
      ↓
Shipment Workflow
      ↓
Data Access
      ↓
SQL Server

and, where required:

Shipment Workflow
      ↓
Carrier Resolver
      ↓
DHL / UPS / DPD / Asendia Adapter
      ↓
Carrier API
      ↓
Standard Shipment Result
                

The value of this structure is not the number of layers. It is the ability to change one part of the platform without spreading that change across every other part.


ASP.NET Core Enterprise Architecture Checklist

  • Are API contracts separated from database entities?
  • Are business workflows separated from infrastructure details?
  • Are external carrier APIs isolated behind integration boundaries?
  • Is dependency direction intentional?
  • Is the data-access strategy appropriate for each workload?
  • Are long-running tasks handled without blocking normal requests?
  • Are database transactions kept focused?
  • Is configuration separated from business logic?
  • Can important workflows be tested without real external services?
  • Can another developer understand the architecture without excessive explanation?

Conclusion

Good ASP.NET Core enterprise architecture is about creating boundaries that protect business workflows from unnecessary coupling. The most valuable boundaries usually separate application behavior from infrastructure, database details and external service contracts.

In shipping software, this becomes particularly important because one business operation can touch SQL Server, data access components and external carrier APIs such as DHL, UPS, DPD and Asendia. A common application model with carrier-specific adapters keeps that complexity manageable.

Whether the implementation uses Dapper, Entity Framework, ADO.NET, background processing or ASP.NET Core APIs, the architectural objective remains the same: make the system easier to change, test, operate and scale without adding unnecessary complexity.


About the Author

I'm Kshitij Mehta, a Senior .NET Developer and ASP.NET Core Consultant with over 14 years of experience designing and developing enterprise software solutions.

My work includes C#, ASP.NET Core, SQL Server, Dapper, Entity Framework, REST APIs, logistics platforms, shipping software, carrier integrations and business applications.

This article focuses on practical ASP.NET Core enterprise architecture patterns that can be applied to real database-driven and integration-heavy .NET systems.