TL;DR - Default Interface Methods vs Abstract Methods
  • Use default interface methods for API evolution and optional behavior without breaking existing code.
  • Use abstract methods and classes for shared state, dependency injection, and complex inheritance.
  • Interfaces support multiple inheritance; abstract classes do not.
  • Abstract classes allow fields, constructors, and access modifiers; interfaces do not.
  • Performance is similar; choose based on design and maintainability.

Default interface methods arrived in C# 8, letting you add implementation directly to interfaces. But when should you use them over traditional abstract methods?

Key Differences

FeatureDefault Interface MethodsAbstract Methods
Access ModifiersPublic onlyPublic, protected, private
State ManagementNo instance fieldsFull field access
Constructor LogicNot supportedComplete constructor support
InheritanceInterface inheritanceClass inheritance
Multiple InheritanceYes (interfaces)No (single class)

Default Interface Methods: API Evolution

Perfect for evolving existing APIs without breaking implementations:

public interface IUserService
{
    Task<User> GetUserAsync(int id);
    
    // Added in v2.0 - doesn't break existing implementations
    public async Task<User[]> GetActiveUsersAsync()
    {
        // Default implementation
        return await GetUsersAsync().Where(u => u.IsActive).ToArrayAsync();
    }
    
    protected virtual Task<IQueryable<User>> GetUsersAsync() => 
        throw new NotImplementedException("Override in implementation");
}

Abstract Methods: Shared State and Logic

Better for complex shared behavior with state management:

public abstract class ServiceBase
{
    protected readonly ILogger _logger;
    protected readonly IConfiguration _config;
    
    protected ServiceBase(ILogger logger, IConfiguration config)
    {
        _logger = logger;
        _config = config;
    }
    
    protected virtual async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation)
    {
        var maxRetries = _config.GetValue<int>("MaxRetries");
        for (int i = 0; i < maxRetries; i++)
        {
            try
            {
                return await operation();
            }
            catch (Exception ex) when (i < maxRetries - 1)
            {
                _logger.LogWarning("Retry {Attempt} failed: {Error}", i + 1, ex.Message);
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
            }
        }
        throw new InvalidOperationException("Max retries exceeded");
    }
    
    public abstract Task<string> ProcessAsync();
}

When to Choose What

Feature / ScenarioDefault Interface MethodsAbstract Methods
API EvolutionAllows evolving APIs without breaking changesAdding methods breaks implementations
Multiple InheritanceSupported (via interfaces)Not supported (single class only)
Optional Behavior ExtensionsCan provide default implementationsMust be implemented in derived class
State ManagementNo instance fieldsFull field and state support
Constructor Dependency InjectionNot supportedSupported
Access ModifiersPublic onlyPublic, protected, private
Complex Shared LogicLimitedSupported
PerformanceVirtual dispatch (optimized)Direct inheritance (optimized)
Best Use CaseAPI evolution, optional featuresShared state, DI, complex logic

Performance Impact

Both approaches have similar performance characteristics. Default interface methods use virtual dispatch, while abstract methods provide direct inheritance. The JIT compiler optimizes both effectively in modern .NET.

Bottom line: Default interface methods excel at API evolution and optional behavior. Abstract methods dominate when you need shared state, dependency injection, and complex inheritance patterns.

See other c-sharp posts