Bootcamp

From idea to product, one lesson at a time. To submit your story: https://tinyurl.com/bootspub1

Follow publication

Why Lazy Loading Isn’t the Bad Guy You Think It Is

Admir Mujkic
Bootcamp
Published in
4 min readDec 3, 2024

In recent years, I’ve noticed an interesting trend in the .NET community. Whenever someone writes about ORMs, I often see advice like:

ORMs are fine, just make sure to disable that pesky feature called Lazy Loading.

Somehow, this functionality has gotten a reputation for being unnecessary and only bringing confusion and performance issues.

Let’s see why I disagree with this view.

A Practical Example

Let’s look at the following domain model:

public class Car : Entity
{
public virtual string Name { get; set; }
public virtual decimal Price { get; set; }

public virtual IList<ServiceRecord> ServiceHistory { get; set; }
public virtual IList<Equipment> InstalledEquipment { get; set; }
}

public class ServiceRecord : Entity
{
public virtual Car Car { get; set; }
public virtual ServiceType Type { get; set; }
public virtual decimal Cost { get; set; }
}

public enum ServiceType
{
OilChange = 1,
TireRotation = 2,
BrakeService = 3,
EngineRepair = 4
}

public class Equipment : Entity
{
public virtual string Name { get; set; }
public virtual int InstallationYear { get; set; }
}

What happens when you need to display a car on a web page and you’re not careful? With lazy loading enabled, you might get the N+1 problem: first loading the car into memory, then all its services, then the service types, and so on until you traverse the entire object graph.

For a car with 5 service records and 5 pieces of equipment, you’ll get a total of 13 database calls:

  • 1 for Car
  • 1 for Car.ServiceHistory
  • 5 for ServiceType in each Car.ServiceHistory element
  • 1 for Car.InstalledEquipment
  • 5 for Equipment in each Car.InstalledEquipment element

This is obviously bad for performance. For a single page view, there should be only one database call. That’s why some suggest turning off Lazy Loading completely.

But is that really the solution?

When Lazy Loading Helps

Let’s take an example of removing equipment from a car:

public IActionResult RemoveEquipment(long carId, int equipmentNumber)
{
Car car = _carRepository.GetById(carId);
Equipment equipment = car.InstalledEquipment[equipmentNumber];
car.InstalledEquipment.Remove(equipment);

return Ok();
}

Which parts of the object graph are affected? Only the car and its equipment:

With lazy loading, we have 2 database calls:

-- Call 1
SELECT *
FROM dbo.Car c
WHERE c.CarId = @CarId

-- Call 2
SELECT *
FROM dbo.Equipment e
WHERE e.CarId = @CarId

Without lazy loading, we would get:

-- Call 1
SELECT *
FROM dbo.Car c
INNER JOIN dbo.Equipment e ON c.CarId = e.CarId
INNER JOIN dbo.ServiceRecord s ON c.CarId = s.CarId
WHERE c.CarId = @CarId

Although we have just one database call, the version without lazy loading transfers much more data between the application and database.

A More Complex Scenario

Let’s take another example. Say you need to add new equipment to a car, but only if its price doesn’t exceed $50,000:

public IActionResult AddEquipment(long carId, string equipmentName)
{
Equipment equipment = _equipmentRepository.GetByName(equipmentName);
Car car = _carRepository.GetById(carId);

if (car.Price > 50000M)
return Error($"Cannot add {equipmentName}, car is too expensive");

car.InstalledEquipment.Add(new Equipment
{
Name = equipmentName,
InstallationYear = DateTime.Now.Year
});

return Ok();
}

With lazy loading, if most cars are too expensive, we’ll typically make just one database call — to retrieve the car. That’s much more efficient than loading the entire object graph every time.

Separating Read and Write Models

The key insight is that lazy loading behaves differently for reads and writes:

  • It’s beneficial for writes (both in terms of performance and code simplicity)
  • It can be problematic for reads

The solution? Don’t use domain classes in the read model. Write SQL queries yourself and materialize data directly to DTOs using a lightweight ORM like Dapper.

This is actually a light application of CQRS (Command Query Responsibility Segregation). You don’t need explicit commands and queries — just follow the simple rule of not using domain classes when selecting data for the UI.

For the End

The criticism of lazy loading is often oversimplified. Performance-wise, it’s about balancing between the amount of data loaded upfront and the number of subsequent database calls. Neither too many database calls nor too much initial loading is ideal.

Here’s what I recommend:

  1. Keep lazy loading for write operations
  2. Use direct SQL/Dapper queries for read operations
  3. Consider CQRS principles for cleaner separation

Your specific needs may vary, but this approach works well for most applications: Entity Framework with lazy loading for writes, and hand-written SQL queries with Dapper for reads.

Performance optimization isn’t about blindly following rules — it’s about understanding your specific use cases and making informed decisions.

Let me know your experience and thoughts in comments.

Cheers and Good Luck! 👋

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Bootcamp
Bootcamp

Published in Bootcamp

From idea to product, one lesson at a time. To submit your story: https://tinyurl.com/bootspub1

Admir Mujkic
Admir Mujkic

Written by Admir Mujkic

I am Admir Mujkić, a technical architect with over 15 years of experience helping businesses harness technology to innovate, grow, and succeed.

No responses yet

Write a response