DEV Community

Dhana
Dhana

Posted on

Controller and Data Access: Understanding 2-Layer Architecture Through Abstraction and Encapsulation

Not every real-world application needs full Clean Architecture with its multiple layers, dependency inversion, and strict domain boundaries. Many production systems — particularly internal enterprise applications like HRMS and Payroll platforms — run successfully on a simpler, pragmatic 2-layer architecture: a Controller layer and a Data Access layer. Understanding why this simpler structure works, and how it relates to the same OOP principles behind Clean Architecture, makes it easier to work confidently in either kind of codebase.

The Two Layers

A 2-layer architecture splits an application into exactly what the name suggests:

Layer 1 — Controller Layer: handles incoming HTTP requests, and delegates the actual work elsewhere.

csharp
[ApiController]
[Route("api/[controller]")]
public class LeaveRequestController : ControllerBase
{
private readonly ILeaveRequestDataAccess _dataAccess;

public LeaveRequestController(ILeaveRequestDataAccess dataAccess)
{
    _dataAccess = dataAccess;
}

[HttpGet("get-emp-details")]
public IActionResult GetEmployeeDetails(string empId)
{
    var result = _dataAccess.GetEmployeeDetails(empId);
    return Ok(result);
}
Enter fullscreen mode Exit fullscreen mode

}

Layer 2 — Data Access Layer: handles the actual database interaction — connection strings, parameters, stored procedure or package calls.

csharp
public interface ILeaveRequestDataAccess
{
EmployeeDto GetEmployeeDetails(string empId);
}

public class LeaveRequestDataAccess : ILeaveRequestDataAccess
{
public EmployeeDto GetEmployeeDetails(string empId)
{
// ADO.NET/Oracle-specific logic lives here:
// connection string, OracleCommand, package name, parameter types
return employeeData;
}
}

The Controller never touches ADO.NET directly. The Data Access layer never touches HTTP concerns like status codes or routing. Each layer has exactly one job.

Why This Is Abstraction and Encapsulation in Practice

This separation isn't just organizational tidiness — it's a direct, practical application of two core OOP principles.

Abstraction: the Controller calls _dataAccess.GetEmployeeDetails(empId) without knowing or caring how that data actually gets retrieved. It doesn't know whether the underlying call uses a stored procedure, a package function, or raw SQL. It only needs to know that calling this method returns the data it asked for. The internal complexity of connection handling, parameter binding, and query execution is hidden behind a simple method signature.

Encapsulation: the Data Access layer protects the details of how data is fetched from being scattered across the codebase. If every controller method directly opened its own OracleConnection and wrote its own SQL, any change to the database — a renamed package, a new parameter, a switched provider — would require hunting down and editing code in dozens of places. By encapsulating that logic inside one dedicated layer, a change only needs to happen once.

The Practical Test: What Happens When the Database Changes

A useful way to confirm this separation is working correctly is to ask: if the underlying database technology changed — say, from Oracle to SQL Server — which layer would need to change?

The answer should be: only the Data Access layer. The connection strings, parameter types, and query syntax living inside LeaveRequestDataAccess would need rewriting. The Controller, which only knows about the ILeaveRequestDataAccess interface and calls GetEmployeeDetails(empId), wouldn't need to change at all — it has no idea, and no need to know, what database sits behind that interface.

If a database change ever required touching Controller code too, that would be a signal the separation isn't clean — some data-access detail has leaked into a layer that shouldn't know about it.

How This Relates to Clean Architecture

Clean Architecture takes this same underlying idea — separating concerns so that change in one area doesn't ripple through the whole system — and extends it further, typically into more layers: a Domain layer (core business rules, independent of any framework), an Application layer (use cases, orchestration), an Infrastructure layer (database, external services), and a Presentation layer (API or UI).

The core principle is identical to the 2-layer setup: outer layers depend on inner layers, never the other way around, and each layer only knows what it strictly needs to know. A 2-layer Controller/Data Access split is, in effect, a simplified version of this same idea — fewer layers, less ceremony, but the same underlying discipline of not letting database-specific details leak into request-handling code, and not letting request-handling concerns leak into data-access code.

For many internal enterprise applications, a full Clean Architecture setup — with separate Domain, Application, and Infrastructure projects, dependency inversion containers, and strict boundary enforcement — is more structure than the project actually needs. A well-maintained 2-layer architecture, with a clear interface between Controller and Data Access, delivers much of the same practical benefit — testability, easier maintenance, isolated change — without the additional overhead.

When 2 Layers Stop Being Enough

The 2-layer pattern works well until business logic starts accumulating inside the Controller itself — validation rules, calculations, workflow decisions that have nothing to do with HTTP handling or database access. When that happens, it's usually a sign that a third layer — often called a Service or Business Logic layer — is needed to hold that logic separately, keeping the Controller thin and focused purely on request/response handling. This is often the natural next step toward something closer to Clean Architecture, added incrementally as a project's complexity genuinely justifies it, rather than adopted wholesale from day one.

Takeaway

A 2-layer Controller/Data Access architecture isn't a simplified shortcut that skips "real" architecture — it's a direct, practical application of Abstraction and Encapsulation at the system level, just as those same principles apply within a single class. Understanding it this way makes the reasoning behind Clean Architecture's more elaborate layering easier to grasp too: both are solving the same underlying problem — isolating change, hiding implementation detail, and keeping each part of the system focused on exactly one responsibility — just at different levels of complexity depending on what the project actually needs.

Top comments (0)