Quick Reference Table

FeatureAbstract ClassInterfaceWhen to Use
InheritanceSingle onlyMultiple allowedAbstract: shared logic; Interface: contracts
ImplementationCan provideContract onlyAbstract: code reuse; Interface: flexibility
ConstructorsSupportedNot allowedAbstract: initialization; Interface: pure contracts
State/FieldsYesNoAbstract: data sharing; Interface: behavior only
PerformanceSlightly fasterVirtual dispatchAbstract: hot paths; Interface: most scenarios
TestingCan be difficultEasy with mocksAbstract: integration tests; Interface: unit tests

Common Pitfalls:

  • “Interfaces are always better” - Wrong when you need shared implementation
  • “Can’t test abstract classes” - Wrong, test through concrete implementations
  • “Default interface methods replace abstract classes” - Wrong, limited capabilities

1. When to Use an Abstract Class vs Interface in C# for OOP Design?

Why this matters: This fundamental decision affects code maintainability, reusability, and team productivity. Wrong choices lead to code duplication or rigid architectures.

Choose abstract classes when you need shared implementation logic and state management across derived types. Interfaces work for contracts, but abstract classes handle common behavior.

Use abstract classes when:

  • You need protected fields or constructor logic
  • Multiple derived classes share the same implementation
  • You want to provide default behavior that can be overridden

Use interfaces when:

  • You need multiple inheritance
  • You want a testable contract without implementation
  • You need to ensure a class adheres to a specific API
  • You want to allow different implementations without forcing a base class hierarchy

Common mistake: Using interfaces when you actually need shared implementation, leading to duplicate code across implementations.

Common mistake: Choosing interfaces when you actually need shared implementation, leading to duplicate code across implementations.

public abstract class PaymentProcessor
{
    protected readonly ILogger _logger;
    protected PaymentProcessor(ILogger logger) => _logger = logger;
    
    public abstract Task<PaymentResult> ProcessAsync(decimal amount);
    
    protected void LogTransaction(decimal amount) => 
        _logger.LogInformation("Processing ${Amount}", amount);
}

public class StripeProcessor : PaymentProcessor
{
    public StripeProcessor(ILogger logger) : base(logger) { }
    
    public override async Task<PaymentResult> ProcessAsync(decimal amount)
    {
        LogTransaction(amount); // Shared implementation
        return await CallStripeApi(amount);
    }
}

Practice: Try designing a file processor system where different formats (CSV, JSON, XML) need shared validation and logging logic.

2. Can a C# Abstract Class Implement Multiple Interfaces and Why It Matters?

Why this matters: This pattern combines the benefits of contracts with shared implementation, reducing code duplication while maintaining interface segregation principles.

Yes, abstract classes can implement multiple interfaces, creating a foundation that combines contracts with shared implementation. This pattern works well for complex domain models.

Common mistake: Implementing multiple interfaces directly in concrete classes instead of using an abstract base, leading to duplicated validation and state management logic.

public interface IValidatable
{
    ValidationResult Validate();
}

public interface IAuditable
{
    DateTime CreatedAt { get; set; }
    string CreatedBy { get; set; }
}

public abstract class Entity : IValidatable, IAuditable
{
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public string CreatedBy { get; set; } = string.Empty;
    
    public virtual ValidationResult Validate() => ValidationResult.Success;
    
    protected abstract void ApplyBusinessRules();
}

public class Product : Entity
{
    public string Name { get; set; } = string.Empty;
    
    protected override void ApplyBusinessRules()
    {
        if (string.IsNullOrEmpty(Name))
            throw new InvalidOperationException("Product name required");
    }
}

Interview scenario: “Design a system where User, Product, and Order entities all need validation, auditing, and caching. How would you structure this?”

3. What happens if two interfaces have the same method signature, how does C# handle it?

Why this matters: Understanding interface collision resolution is crucial for API design and prevents unexpected behavior when combining multiple interface contracts.

C# allows implicit implementation when method signatures match exactly. The same method satisfies both interface contracts without explicit interface implementation.

Common mistake: Assuming this causes compilation errors. C# handles it gracefully, but you need explicit implementation for different behaviors.

public interface IReader
{
    Task<string> ReadAsync();
}

public interface IProcessor
{
    Task<string> ReadAsync(); // Same signature
}

public class FileHandler : IReader, IProcessor
{
    // Single method satisfies both interfaces
    public async Task<string> ReadAsync()
    {
        return await File.ReadAllTextAsync("data.txt");
    }
}

// Both calls use the same method
IReader reader = new FileHandler();
IProcessor processor = new FileHandler();
await reader.ReadAsync();    // Same implementation
await processor.ReadAsync(); // Same implementation

If you need different behavior per interface, use explicit implementation:

public class FileHandler : IReader, IProcessor
{
    async Task<string> IReader.ReadAsync() => await File.ReadAllTextAsync("input.txt");
    async Task<string> IProcessor.ReadAsync() => await File.ReadAllTextAsync("config.txt");
}

Practice: Write a class implementing both IDataSource and IConfigSource with the same GetValue(string key) method but different behaviors.

4. How do default interface methods compare to abstract class methods in C# 8+?

Default interface methods provide implementation in interfaces, but abstract class methods offer more flexibility with access modifiers and state management.

FeatureDefault Interface MethodsAbstract Class Methods
Access ModifiersPublic onlyPublic, protected, private
State AccessNo instance fieldsFull field access
Constructor LogicNot availableFull constructor support
Override BehaviorCan be overriddenCan be overridden
// Default interface method
public interface ICache
{
    Dictionary<string, object> Items => new();
    
    // Default implementation - public only
    public T GetOrAdd<T>(string key, Func<T> factory)
    {
        return Items.ContainsKey(key) ? (T)Items[key] : factory();
    }
}

// Abstract class method
public abstract class CacheBase
{
    protected readonly ConcurrentDictionary<string, object> _items = new();
    
    protected virtual T GetOrAdd<T>(string key, Func<T> factory)
    {
        return (T)_items.GetOrAdd(key, _ => factory()!);
    }
}

Abstract classes provide better encapsulation and state management for complex shared behavior.

default interface != abstract replacement.

Few limitations:

  • No state or fields
  • No access to private members
  • Breaks LSP if abused

5. When designing a plugin system, is interface or abstract class a better fit?

Why this matters: Plugin architectures require extensibility without tight coupling. The wrong choice locks you into rigid inheritance hierarchies that limit third-party contributions.

Interfaces work better for plugin systems because they provide contract-based extensibility without forcing inheritance hierarchies. Plugins need flexibility, not rigid inheritance.

Common mistake: Using abstract base classes for plugins, which prevents implementers from using their own base classes and limits flexibility.

public interface IPlugin
{
    string Name { get; }
    Version Version { get; }
    Task InitializeAsync(IServiceProvider services);
    Task<PluginResult> ExecuteAsync(PluginContext context);
}

public class PluginManager
{
    private readonly List<IPlugin> _plugins = new();
    
    public async Task LoadPluginAsync<T>() where T : IPlugin, new()
    {
        var plugin = new T();
        await plugin.InitializeAsync(_serviceProvider);
        _plugins.Add(plugin);
    }
    
    public async Task<IEnumerable<PluginResult>> ExecuteAllAsync(PluginContext context)
    {
        var tasks = _plugins.Select(p => p.ExecuteAsync(context));
        return await Task.WhenAll(tasks);
    }
}

public class EmailPlugin : IPlugin
{
    public string Name => "Email Notifications";
    public Version Version => new(1, 0, 0);
    
    public Task InitializeAsync(IServiceProvider services) => Task.CompletedTask;
    public async Task<PluginResult> ExecuteAsync(PluginContext context) => 
        await SendEmailAsync(context.Data);
}

Interview scenario: “You’re building a text editor that needs third-party extensions for syntax highlighting, code formatting, and language servers. Interface or abstract class?”

6. Can you inject an abstract class using dependency injection in ASP.NET Core?

No, you cannot directly inject abstract classes. DI containers need concrete implementations. Register concrete types that inherit from your abstract class instead.

public abstract class NotificationService
{
    protected readonly ILogger _logger;
    protected NotificationService(ILogger logger) => _logger = logger;
    
    public abstract Task SendAsync(string message, string recipient);
}

public class EmailNotificationService : NotificationService
{
    public EmailNotificationService(ILogger<EmailNotificationService> logger) : base(logger) { }
    
    public override async Task SendAsync(string message, string recipient)
    {
        _logger.LogInformation("Sending email to {Recipient}", recipient);
        await SendEmailAsync(message, recipient);
    }
}

// Program.cs - Register concrete implementation
builder.Services.AddScoped<EmailNotificationService>();

// Controller - Inject concrete type
public class NotificationsController : ControllerBase
{
    private readonly EmailNotificationService _notificationService;
    
    public NotificationsController(EmailNotificationService notificationService)
    {
        _notificationService = notificationService;
    }
}

For multiple implementations, use interfaces with abstract base classes for shared logic.

Read more about Dependency Injection in ASP.NET Core to understand how to structure your services effectively.

7. How does versioning differ when using abstract classes vs interfaces in libraries?

Interface changes break binary compatibility, while abstract class changes can maintain compatibility through virtual methods and new overloads.

Interface versioning challenges:

// Version 1.0
public interface IUserService
{
    Task<User> GetUserAsync(int id);
}

// Version 2.0 - BREAKING CHANGE
public interface IUserService
{
    Task<User> GetUserAsync(int id);
    Task<User> GetUserAsync(string email); // New method breaks existing implementations
}

Abstract class versioning flexibility:

// Version 1.0
public abstract class UserServiceBase
{
    public abstract Task<User> GetUserAsync(int id);
}

// Version 2.0 - NON-BREAKING
public abstract class UserServiceBase
{
    public abstract Task<User> GetUserAsync(int id);
    
    // Virtual method with default implementation
    public virtual async Task<User> GetUserAsync(string email)
    {
        var users = await GetAllUsersAsync();
        return users.FirstOrDefault(u => u.Email == email);
    }
    
    protected virtual Task<User[]> GetAllUsersAsync() => Task.FromResult(Array.Empty<User>());
}

Abstract classes provide better versioning strategies for evolving APIs.

8. What are the performance implications of using interfaces vs abstract classes in hot paths?

Why this matters: In performance-critical applications (gaming, real-time systems, high-frequency trading), method dispatch overhead can accumulate and impact user experience or system throughput.

In performance-critical code, like tight loops or real-time processing, the way methods are dispatched matters.

Interfaces in C# use interface dispatch, which involves an extra level of indirection through a virtual method table (vtable). This can prevent the JIT compiler from inlining those calls, leading to slightly higher call latency. It also makes CPU branch prediction less efficient, especially when the call target isn’t predictable.

Abstract classes, on the other hand, use virtual method dispatch, which is generally more JIT-friendly. If you mark overrides as sealed, the JIT can often inline those calls, resulting in better performance.

In most applications, this overhead is negligible. But in hot paths, like game loops, high-frequency trading systems, or large-scale data processing, the difference can add up.

Common mistake: Premature optimization - choosing abstract classes for performance when the difference doesn’t matter in your specific use case.

Benchmark results (method calls per second):

MethodMeanNotes
InterfaceCall589.7 ns
AbstractClassCall566.1 ns~4% faster

Bottom line: Choose based on design needs first, then consider performance in verified hot paths.

9. Why might you use an interface for mocking but an abstract class for inheritance?

Interfaces provide clean mocking surfaces without implementation baggage, while abstract classes share real business logic across derived types.

// Interface for mocking - clean contract
public interface IPaymentGateway
{
    Task<PaymentResponse> ChargeAsync(decimal amount, string token);
}

// Abstract class for shared logic
public abstract class PaymentProcessorBase
{
    protected readonly IPaymentGateway _gateway;
    protected readonly ILogger _logger;
    
    protected PaymentProcessorBase(IPaymentGateway gateway, ILogger logger)
    {
        _gateway = gateway;
        _logger = logger;
    }
    
    protected async Task<bool> ValidateAndProcessAsync(PaymentRequest request)
    {
        if (!ValidateRequest(request)) return false;
        
        var response = await _gateway.ChargeAsync(request.Amount, request.Token);
        LogResult(response);
        return response.Success;
    }
    
    protected abstract bool ValidateRequest(PaymentRequest request);
}

// Unit test - mock interface easily
var mockGateway = new Mock<IPaymentGateway>();
mockGateway.Setup(g => g.ChargeAsync(It.IsAny<decimal>(), It.IsAny<string>()))
          .ReturnsAsync(new PaymentResponse { Success = true });

Interfaces enable testability, abstract classes enable code reuse.

10. How do constructors behave differently in interfaces and abstract classes?

Interfaces cannot have constructors, while abstract classes support constructors for initialization logic that derived classes must call.

// Interface - no constructor support
public interface IRepository
{
    Task<T> GetByIdAsync<T>(int id);
}

// Abstract class - constructor for shared initialization
public abstract class RepositoryBase
{
    protected readonly DbContext _context;
    protected readonly ILogger _logger;
    
    // Constructor ensures proper initialization
    protected RepositoryBase(DbContext context, ILogger logger)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
    
    protected virtual async Task<T> GetEntityAsync<T>(int id) where T : class
    {
        _logger.LogDebug("Fetching entity {Type} with ID {Id}", typeof(T).Name, id);
        return await _context.Set<T>().FindAsync(id);
    }
}

public class UserRepository : RepositoryBase
{
    // Must call base constructor
    public UserRepository(DbContext context, ILogger<UserRepository> logger) 
        : base(context, logger) { }
}

Abstract class constructors enforce proper dependency injection and initialization patterns across inheritance hierarchies.

About the Author

Abhinaw Kumar is a software engineer who builds real-world systems: from resilient ASP.NET Core backends to clean, maintainable Angular frontends. With over 11+ years in production development, he shares what actually works when you're shipping software that has to last.

Read more on the About page or connect on LinkedIn.

Additional Interview Preparation Resources

Official Microsoft Documentation

Related Posts