Service Layer
A controller should be a receptionist: take the request, pass it to the right person, give back the answer. The service layer is that person — it holds the business rules.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Explain controller → service → repository layering
- Move business rules out of controllers
- Signal business failures from services with exceptions or results
- Translate service outcomes into status codes
Explanation#
Controllers handle HTTP: binding, status codes, headers. Services handle business rules: “you can order 1–10 items”, “stock cannot go negative”. Repositories handle data access.
Services know nothing about HTTP. They do not return IHttpActionResult or read Request. That makes them reusable (from a background job, for example) and easy to unit test.
Services report problems with specific exceptions (NotFoundException, BusinessRuleException) or result objects. The controller — or a global filter — turns them into 404, 400 or 409.
Each layer depends on the interface of the next (IOrderService, IProductRepository), injected by the DI container.
A typical project has folders Controllers, Services, Data (or Repositories) and Models/Dtos.
Syntax#
HTTP request
↓
Controller — binding, status codes (OrdersController)
↓
Service — business rules (IOrderService / OrderService)
↓
Repository — SQL / EF6 (IProductRepository)
↓
SQL Server
Code example: Placing an order through a service#
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using ShopApi.Data; // IProductRepository
public class NotFoundException : Exception
{
public NotFoundException(string message) : base(message) { }
}
public class BusinessRuleException : Exception
{
public BusinessRuleException(string message) : base(message) { }
}
public interface IOrderService
{
Task<int> PlaceOrderAsync(int productId, int quantity);
}
// Business rules live here — no HTTP in sight.
public class OrderService : IOrderService
{
private static int _lastOrderId = 1000;
private readonly IProductRepository _products;
public OrderService(IProductRepository products)
{
_products = products;
}
public async Task<int> PlaceOrderAsync(int productId, int quantity)
{
if (quantity < 1 || quantity > 10)
throw new BusinessRuleException("You can order between 1 and 10 items.");
var product = await _products.GetByIdAsync(productId);
if (product == null)
throw new NotFoundException("Product " + productId + " was not found.");
if (product.Stock < quantity)
throw new BusinessRuleException("Only " + product.Stock + " left in stock.");
product.Stock -= quantity;
await _products.UpdateAsync(product);
return Interlocked.Increment(ref _lastOrderId);
}
}
public class PlaceOrderRequest
{
public int ProductId { get; set; }
public int Quantity { get; set; }
}
// The controller only translates between HTTP and the service.
public class OrdersController : ApiController
{
private readonly IOrderService _orders;
public OrdersController(IOrderService orders)
{
_orders = orders;
}
[HttpPost, Route("api/orders")]
public async Task<IHttpActionResult> Place(PlaceOrderRequest request)
{
try
{
var orderId = await _orders.PlaceOrderAsync(request.ProductId, request.Quantity);
return Created("api/orders/" + orderId, new { orderId });
}
catch (NotFoundException ex)
{
return Content(HttpStatusCode.NotFound, new { message = ex.Message });
}
catch (BusinessRuleException ex)
{
return Content(HttpStatusCode.Conflict, new { message = ex.Message });
}
}
}
POST /api/orders {"ProductId":1,"Quantity":2} → 201 {"orderId":1001}
POST /api/orders {"ProductId":1,"Quantity":50} → 409 {"message":"You can order between 1 and 10 items."}
POST /api/orders {"ProductId":77,"Quantity":1} → 404 {"message":"Product 77 was not found."}
POST /api/orders {"ProductId":1,"Quantity":9} → 409 {"message":"Only 38 left in stock."} (when stock is 38)This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- OrderService checks quantity, existence and stock — all business rules, no HTTP.
- It depends on IProductRepository, so tests can pass a fake repository.
- Failures are thrown as meaningful exception types.
- OrdersController catches those types and chooses 404 or 409; success returns 201.
- Later, a global exception filter can do this mapping for every controller (see Global Exception Handling).
Real-world use case#
The same IOrderService is used by the Web API, a nightly Windows Service that re-processes failed orders, and the unit tests — because it never touches HttpContext or IHttpActionResult.
Common mistakes#
Rules spread across controllers get duplicated and drift apart. Put each rule in one service method.
That ties business logic to Web API. Return data, result objects or throw domain exceptions.
A service that only calls the repository with no rules adds a layer without value. Add it when rules appear — or keep it thin but consistent across the codebase.
Best practices#
- Controllers: HTTP only
- Services: business rules, depend on repository interfaces
- Use specific exception types or result objects for failures
- Unit test services without a web server
Remember this#
Controller = receptionist. Service = expert. Repository = filing cabinet.
Services never mention HTTP.
Service throws NotFound → controller returns 404.
Practice exercise#
Your task: Write an ICouponService with ApplyCoupon(string code, decimal total) that throws BusinessRuleException for unknown codes and returns the discounted total for "SAVE10" (10% off). Write a controller action that returns 200 with the new total or 400 with the message.
Need a hint?
Show solution
using System;
using System.Web.Http;
public class BusinessRuleException : Exception { public BusinessRuleException(string m) : base(m) { } }
public interface ICouponService { decimal ApplyCoupon(string code, decimal total); }
public class CouponService : ICouponService
{
public decimal ApplyCoupon(string code, decimal total)
{
if (code != "SAVE10") throw new BusinessRuleException("Unknown coupon " + code + ".");
return total * 0.9m;
}
}
public class CouponsController : ApiController
{
private readonly ICouponService _coupons;
public CouponsController(ICouponService coupons) { _coupons = coupons; }
[HttpGet, Route("api/coupons/{code}/apply")]
public IHttpActionResult Apply(string code, decimal total)
{
try { return Ok(new { total = _coupons.ApplyCoupon(code, total) }); }
catch (BusinessRuleException ex) { return BadRequest(ex.Message); }
}
}
Mini quiz#
Where should the rule “orders are limited to 10 items” live?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.