DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Best Ways to Write Clean and Quality Python Code
  • Building Internal Developer Platforms on Kubernetes: The Abstraction Problem Nobody Warns You About
  • AI-Assisted Software Engineering With OOPS, SOLID Principles, and Documentation
  • VB6 vs. C#: How to Migrate and Modernize Your Legacy Code

Trending

  • Full-Text Search Meets the Graph: Apache Lucene Inside Neo4j
  • Kubernetes Rolling Updates Are Lying to You — and Your Users Are Paying for It
  • Slopsquatting: Building a Scanner That Catches AI-Hallucinated Packages Before They Reach Production
  • Architecture Lessons from Two Digital Transformations

Draft This article hasn't been published, so readers can't see it.

In moderation This article is waiting for review, so readers can't see it yet.

Deleted This article has been deleted, so readers can't see it.

  1. DZone
  2. Coding
  3. Frameworks
  4. Controller and Data Access: Understanding Two-Layer Architecture Through Abstraction and Encapsulation

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

Learn how a two-layer Controller and Data Access architecture applies abstraction and encapsulation to simplify maintenance and testing in C# applications.

By 
Dhana PAMULA
·
Nov. 13, 26 · Analysis
Post on X (opens in a new tab) Share on LinkedIn (opens in a new tab)
108 Views

Join the DZone community Save articles, join the discussion, and download Refcards and Trend Reports with a free account.

Join for free Already a member? Log in

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 two-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 two-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.

C#
 
[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);
    }
}


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

C#
 
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 two-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 two-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 two-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 Two Layers Stop Being Enough

The two-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.

Why This Split Also Makes Testing Easier

A practical benefit of separating Controller and Data Access that often goes unmentioned until it's needed: this structure makes unit testing significantly more straightforward.

Because the Controller depends on ILeaveRequestDataAccess — an interface — rather than a concrete class tied to Oracle, it becomes possible to test the Controller's logic in isolation, without touching a real database at all. A test can supply a fake or mock implementation of the interface:

C#
 
public class FakeLeaveRequestDataAccess : ILeaveRequestDataAccess
{
    public EmployeeDto GetEmployeeDetails(string empId)
    {
        return new EmployeeDto { EmpId = empId, Name = "Test Employee", Salary = 50000 };
    }
}


With this fake in place, a test can verify that GetEmployeeDetails on the Controller returns the correct HTTP response, handles a missing employee correctly, or triggers the right status code — all without a live Oracle connection, without test data cluttering a real database, and without the test slowing down because it's waiting on network or database round-trips.

This is only possible because the Controller was written to depend on an abstraction (ILeaveRequestDataAccess) rather than directly instantiating new LeaveRequestDataAccess() inside itself. If the Controller created its own Data Access instance internally, there would be no way to substitute a fake version during testing — the real database dependency would be unavoidable. This is the same underlying idea, applied practically: depending on an interface rather than a concrete implementation isn't just an architectural preference; it's what makes isolated, fast, reliable testing possible at all.

Takeaway

A two-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.

Object-oriented programming Abstraction (computer science) Encapsulation (networking)

Opinions expressed by DZone contributors are their own.

Related

  • Best Ways to Write Clean and Quality Python Code
  • Building Internal Developer Platforms on Kubernetes: The Abstraction Problem Nobody Warns You About
  • AI-Assisted Software Engineering With OOPS, SOLID Principles, and Documentation
  • VB6 vs. C#: How to Migrate and Modernize Your Legacy Code

Partner Resources

×

Comments

Loading comments

The comments didn't load

Please try again in a moment.

Likes

Loading

The next likes didn't load. Please try again.

The likes didn't load

Please try again in a moment.

See who liked this

Log in or join DZone for free to see who liked this article.

Log in Join for free
  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook