day 07: Interface & Dependency Inversion
Decoupling services from concrete implementations using contracts
Introduction
Up to this point, the system worked well. However, the Service layer was still tightly coupled to a concrete Repository implementation.
This step introduces interfaces and dependency inversion to ensure that the Service does not depend on a specific storage mechanism.
🌊 System Flow
- Request: The Controller receives a request.
- Delegation: The Controller forwards the request to the Service.
-
Dependency Inversion:
The Service does not know the concrete repository.
It only calls methods defined in
IHelloRepository. -
Execution:
Program.csinjects the actual implementation (HelloRepository) that fulfills the contract.
1️⃣ Model: Data Contract
The model defines the shape of the data exchanged across layers.
namespace HelloFlow.Models;
public class HelloResponse
{
public string Message { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public string Location { get; set; } = string.Empty;
}
2️⃣ Repository Contract: IHelloRepository
The interface acts as a contract. It defines what must be done, not how it is done.
using HelloFlow.Models;
namespace HelloFlow.Repositories;
public interface IHelloRepository
{
void Save(HelloResponse data);
List<HelloResponse> GetAll();
List<HelloResponse> SearchAdvanced(string keyword, int pageNumber, int pageSize);
}
Any repository implementation that follows this contract can be used interchangeably.
3️⃣ Repository Implementation
This class fulfills the contract using an in-memory, thread-safe data structure.
using System.Collections.Concurrent;
using HelloFlow.Models;
namespace HelloFlow.Repositories;
public class HelloRepository : IHelloRepository
{
private readonly ConcurrentDictionary<string, HelloResponse> _storage = new();
public void Save(HelloResponse data)
{
var key = Guid.NewGuid().ToString();
_storage.TryAdd(key, data);
}
public List<HelloResponse> GetAll()
{
return _storage.Values.ToList();
}
public List<HelloResponse> SearchAdvanced(string keyword, int pageNumber, int pageSize)
{
var query = _storage.Values.AsEnumerable();
if (!string.IsNullOrWhiteSpace(keyword))
{
query = query.Where(x =>
x.Message.Contains(keyword, StringComparison.OrdinalIgnoreCase));
}
return query
.OrderByDescending(x => x.CreatedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToList();
}
}
4️⃣ Service Layer: Depending on the Contract
The Service no longer depends on a concrete repository.
It works with IHelloRepository, not HelloRepository.
using HelloFlow.Models;
using HelloFlow.Repositories;
namespace HelloFlow.Services;
public class HelloService
{
private readonly IHelloRepository _repository;
public HelloService(IHelloRepository repository)
{
_repository = repository;
}
public HelloResponse GetHello(string name)
{
var response = new HelloResponse
{
Message = $"Hello, {name}!",
CreatedAt = DateTime.Now,
Location = "Cazis, Switzerland"
};
_repository.Save(response);
return response;
}
public List<HelloResponse> FindHelloAdvanced(string keyword, int page, int size)
{
int safePage = page < 1 ? 1 : page;
int safeSize = size > 50 ? 50 : size;
return _repository.SearchAdvanced(keyword, safePage, safeSize);
}
}
This change represents the core of dependency inversion: the Service depends on an abstraction, not a concrete implementation.
5️⃣ Controller: Request Handling Only
using Microsoft.AspNetCore.Mvc;
using HelloFlow.Services;
namespace HelloFlow.Controllers;
[ApiController]
[Route("api/[controller]")]
public class HelloController : ControllerBase
{
private readonly HelloService _service;
public HelloController(HelloService service)
{
_service = service;
}
[HttpGet]
public IActionResult SayHello(string name)
{
var result = _service.GetHello(name);
return Ok(result);
}
[HttpGet("search")]
public IActionResult SearchHello(
[FromQuery] string? name,
[FromQuery] int page = 1,
[FromQuery] int size = 10
)
{
var results = _service.FindHelloAdvanced(name ?? "", page, size);
return Ok(results);
}
}
6️⃣ Program.cs: Dependency Wiring
This is where the abstraction is connected to its concrete implementation.
using HelloFlow.Services;
using HelloFlow.Repositories;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Interface → Implementation mapping
builder.Services.AddSingleton<IHelloRepository, HelloRepository>();
builder.Services.AddScoped<HelloService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
🧠 Why This Matters
By introducing an interface:
- The Service does not care how data is stored
- The Repository can be replaced without changing Service logic
- Future database integration becomes trivial
Only the DI configuration changes when swapping implementations.
🧠 One-Sentence Summary
This step is not about writing more code, but about designing a system that can change without breaking.
✍️ My Notes & Reflections
- Each step adds more complexity, but it also gives me a stronger sense of structure.
- It feels as if I am slowly watching a factory come to life, understanding how its parts begin to move and connect with each other.
Top comments (0)