DEV Community

Renato Silva
Renato Silva

Posted on

I Failed a Technical Interview After 10 Years. Here's the Feedback.

I have more than 10 years in software engineering. I've migrated monoliths to .NET Core, designed APIs, tuned SQL Server queries in production, and mentored other developers.

And I just failed a technical interview on the fundamentals.

Not on distributed systems. Not on system design. On Dependency Injection, SOLID, interfaces versus abstract classes, variable scope, and database indexes. The things we expect a mid-level developer to explain.

This post is about that rejection, what I think actually went wrong, and the refresher I wrote for myself afterwards. If you're a senior developer preparing for interviews, this one is for you. 👇

📩 The feedback

A few days after the interview, the email arrived. To their credit, it was specific:

During the technical discussion, we identified some areas where your knowledge could be further strengthened, particularly around a number of fundamental software engineering concepts. These included Dependency Injection, object-oriented programming (OOP) and SOLID principles, the differences between abstract classes and interfaces, variable scope, and database indexing concepts such as clustered and non-clustered indexes.

My first reaction was defensive. I use DI every day. I've built entire systems on these principles.

My second reaction, a day later, was more honest: they weren't wrong about what they saw in that room.

🧠 Doing is not the same as explaining

After 10 years, most fundamentals stop being concepts and become reflexes. I register services in Program.cs without thinking about why constructor injection beats a service locator. I add an index when a query is slow, without reciting the difference between a clustered and a non-clustered one.

That's fine on the job. In an interview, it's a problem. The interviewer can't see your reflexes. They only see what you can put into words, under pressure, in a few minutes.

A few things I noticed when I replayed the interview in my head:

  • I answered with experience instead of definitions. "In my last project we..." is a great follow-up, but a weak opening. Lead with the concept, then prove it with a story.
  • I skipped the vocabulary. Saying "we just inject it" isn't the same as naming the Dependency Inversion Principle and the lifetimes involved.
  • I underestimated the basics. I prepared for system design and architecture. I didn't revise what I assumed I already knew.

Seniority doesn't exempt you from fundamentals. If anything, it raises the bar: a senior is expected to teach them.

📚 The refresher I wrote for myself

Here is how I'd answer each topic now: definition first, then the detail that shows you've actually used it.

1. Dependency Injection

Definition: a class receives its dependencies from the outside instead of creating them itself. It's the practical way to apply the Dependency Inversion Principle: depend on abstractions, not concrete implementations.

public class OrderService
{
    private readonly IPaymentGateway _payments;

    // The dependency is injected, not created with 'new'
    public OrderService(IPaymentGateway payments) => _payments = payments;
}

// Program.cs
builder.Services.AddScoped<IPaymentGateway, StripeGateway>();
builder.Services.AddScoped<OrderService>();
Enter fullscreen mode Exit fullscreen mode

The senior-level detail: service lifetimes in .NET.

  • Transient: a new instance every time it's requested.
  • Scoped: one instance per request (or per scope).
  • Singleton: one instance for the whole application.

The classic trap is a captive dependency: injecting a scoped service (like a DbContext) into a singleton. The scoped service then lives as long as the singleton, which breaks its assumptions.

2. OOP and SOLID

The four pillars of OOP:

  • Encapsulation: hide internal state, expose behaviour.
  • Abstraction: expose what an object does, not how.
  • Inheritance: reuse and specialise behaviour from a base type.
  • Polymorphism: treat different types through a common contract.

SOLID, in one line each:

  • Single Responsibility: a class should have one reason to change.
  • Open/Closed: open for extension, closed for modification.
  • Liskov Substitution: a subtype must be usable wherever its base type is expected, without surprises.
  • Interface Segregation: many small, focused interfaces beat one large one.
  • Dependency Inversion: high-level modules depend on abstractions, not on low-level details.

The senior-level detail: have one real example per principle ready. For Liskov, the classic is the Square that inherits from Rectangle and breaks when you set width and height independently.

3. Abstract classes vs interfaces

Abstract class Interface
Inheritance A class can inherit only one A class can implement many
State (fields) Yes No instance fields
Constructors Yes No
Implementation Abstract and concrete members Contract only (plus default methods since C# 8)
Models An "is-a" relationship with shared code A "can-do" capability
public abstract class Notification
{
    public DateTime CreatedAt { get; } = DateTime.UtcNow; // shared state
    public abstract Task SendAsync();                     // must be implemented
}

public interface IRetryable
{
    int MaxRetries { get; }
}

public class EmailNotification : Notification, IRetryable
{
    public int MaxRetries => 3;
    public override Task SendAsync() => Task.CompletedTask;
}
Enter fullscreen mode Exit fullscreen mode

The senior-level detail: default interface methods blurred the line in C# 8, but the rule of thumb still holds. Use an abstract class to share code and state within a family of types. Use an interface to define a capability that unrelated types can share.

4. Variable scope

Definition: scope is the region of code where a variable can be accessed. In C#, scope is defined by the block ({ }) where the variable is declared.

The interesting part is closures. A lambda captures the variable, not its value at that moment. In JavaScript, this is the classic var vs let question:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i)); // 3, 3, 3 — 'var' is function-scoped
}

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j)); // 0, 1, 2 — 'let' is block-scoped
}
Enter fullscreen mode Exit fullscreen mode

The senior-level detail: explain why. With var, there is one variable shared by every iteration, and it's hoisted to the function. With let, each iteration gets a new binding. C# had the same issue with foreach until C# 5, which changed it so each iteration gets a fresh variable.

5. Clustered vs non-clustered indexes

Clustered index: defines the physical order of the rows in the table. The table is the index. That's why a table can have only one. In SQL Server, the primary key creates a clustered index by default.

Non-clustered index: a separate structure that stores the indexed columns plus a pointer back to the row. A table can have many.

-- Clustered: rows are stored ordered by OrderId
CREATE CLUSTERED INDEX IX_Orders_OrderId ON dbo.Orders (OrderId);

-- Non-clustered, covering the query with INCLUDE
CREATE NONCLUSTERED INDEX IX_Orders_CustomerId
    ON dbo.Orders (CustomerId)
    INCLUDE (OrderDate, Total);
Enter fullscreen mode Exit fullscreen mode

Think of a book: the clustered index is the order of the pages, the non-clustered index is the index at the back that tells you which page to go to.

The senior-level detail: when a non-clustered index doesn't have every column the query needs, SQL Server does a key lookup back to the clustered index for each row. With many rows, that gets expensive. INCLUDE columns make the index covering and remove the lookup. The trade-off: every index speeds up reads and slows down writes.

🛠️ What I'm doing about it

Feeling bad about it for a week wouldn't change anything. So I turned the feedback into a plan:

  1. Explain out loud. For each topic, I say the definition in under 30 seconds, then add one real example from my work. If I can't do it without notes, I'm not ready.
  2. Use the structure "concept → detail → story". Definition first, the senior-level nuance second, and a production story last.
  3. Write about it. Writing this post was part of the plan. If you can teach it, you know it.
  4. Revise the basics before every interview. Not just system design. One evening for fundamentals, every time.

💡 Final thoughts

Failing an interview after 10 years hurts more than failing one after two. But clear, specific feedback is rare, and it's a gift. Most rejections just say "we decided to move forward with other candidates."

If you take one thing from this post: experience doesn't replace fundamentals, it should make you better at explaining them.

Now I'm curious 👇

  • Have you ever failed an interview on something you use every day?
  • Which "basic" question do you think catches senior developers the most?

Let me know in the comments. And if this helped, share it with someone who's preparing for interviews right now.

Top comments (0)