Day 12: Interfaces - Signing Contracts Your Classes Actually Have to Follow
arrow_back All Posts
April 06, 2026 7 min read .NET/C#

Day 12: Interfaces - Signing Contracts Your Classes Actually Have to Follow

Yesterday you learned about inheritance — one class saying "I'm based on you, Mom." Today we flip the script. Interfaces aren't about family trees; they're about contracts. Think of an interface as a legal document that says, "If you want to call yourself an ICanFly, you'd better implement a Fly() method — or the compiler will see you in court."

1. What Is an Interface?

An interface is a promise. It says, "Any class that implements me guarantees it will have these methods and properties." It doesn't say how — that's your problem.

Think of it like a job listing:

Wanted: Someone who can Cook(), Clean(), and DoLaundry(). We don't care how you do them. We just need them done.

Any class that "signs" this contract (implements the interface) is telling the world: "I can do all of those things." The interface doesn't contain a single line of actual logic — it's purely a specification.

If inheritance is "is-a" (a Dog is an Animal), then interfaces are "can-do" (a Dog can IFetch, IBark, and IBeAdorable).

2. Your First Interface

// Define the contract
interface IGreeter
{
    string Greet(string name);
}

// Sign the contract
class FriendlyGreeter : IGreeter
{
    public string Greet(string name)
    {
        return $"Hey {name}! Great to see you! 🎉";
    }
}

class FormalGreeter : IGreeter
{
    public string Greet(string name)
    {
        return $"Good day, {name}. I trust you are well.";
    }
}

// Use the contract
IGreeter greeter = new FriendlyGreeter();
Console.WriteLine(greeter.Greet("Farhad"));
// Output: Hey Farhad! Great to see you! 🎉

greeter = new FormalGreeter();
Console.WriteLine(greeter.Greet("Farhad"));
// Output: Good day, Farhad. I trust you are well.

Let's break it down:

  • interface IGreeter — declares the contract. By convention, interface names start with a capital I. This isn't optional in C# land; skip the I and your teammates will look at you like you just microwaved fish in the office.
  • class FriendlyGreeter : IGreeter — this class implements the interface. It must provide a body for every member declared in IGreeter.
  • IGreeter greeter = ... — you can store any implementing class in an interface-typed variable. This is polymorphism again, just like yesterday — but without needing a shared base class.

3. Interface Rules — The Fine Print

Here's what the compiler enforces:

  • No instance fields. Interfaces can't hold data. They describe behavior, not state.
  • Members are public by default. You don't write public in the interface — it's implied.
  • A class MUST implement every member. Miss one and the compiler throws a tantrum.
  • A class can implement multiple interfaces. Unlike inheritance (one base class only), you can sign as many contracts as you like.
  • Interfaces can inherit from other interfaces. Contracts can extend contracts.
interface IAnimal
{
    string Name { get; }
    void Speak();
}

interface IPet : IAnimal
{
    void ComeWhenCalled();
}

class Dog : IPet
{
    public string Name => "Rex";
    public void Speak() => Console.WriteLine("Woof!");
    public void ComeWhenCalled() => Console.WriteLine("*sprints toward you*");
}

Dog must implement everything from IPet and IAnimal because IPet extends IAnimal. Contracts are cumulative — you can't dodge the parent's terms.

4. Multiple Interfaces — The Real Superpower

Remember how C# only allows single inheritance? One base class, that's it. Interfaces blow that limitation wide open:

interface ICanSwim
{
    void Swim();
}

interface ICanFly
{
    void Fly();
}

interface ICanWalk
{
    void Walk();
}

class Duck : ICanSwim, ICanFly, ICanWalk
{
    public void Swim() => Console.WriteLine("Paddle paddle 🦆");
    public void Fly() => Console.WriteLine("Flap flap!");
    public void Walk() => Console.WriteLine("Waddle waddle");
}

A Duck can swim, fly, and walk. Try doing that with single inheritance — you'd need some terrifying SwimmingFlyingWalkingAnimalBase class. Interfaces keep things modular and clean.

You can pass a Duck anywhere an ICanSwim is expected:

void GoSwimming(ICanSwim swimmer)
{
    swimmer.Swim();
}

GoSwimming(new Duck()); // Paddle paddle 🦆

The method doesn't care that it's a Duck. It only cares that the object can swim. This is called programming to an interface and it's a foundational design principle.

5. Interface vs Abstract Class — The Eternal Debate

This comes up in every C# interview ever, so let's nail it:

  • Abstract class: Use when classes share a common identity and some implementation. A Dog is an Animal.
  • Interface: Use when unrelated classes share a common capability. A Duck and a Submarine can both implement ICanSwim, but they have nothing else in common.

Key differences:

  • A class can inherit from one abstract class but implement many interfaces.
  • Abstract classes can have fields, constructors, and implemented methods. Interfaces traditionally cannot (with one exception — see next section).
  • Abstract classes can have protected members. Interface members are always effectively public.

Rule of thumb: Start with an interface. Only reach for an abstract class when you genuinely need shared state or implementation that child classes inherit.

6. Default Interface Methods (C# 8+)

Here's the plot twist. Since C# 8, interfaces can actually contain method implementations:

interface ILogger
{
    void Log(string message);

    // Default implementation — classes get this for free
    void LogError(string message)
    {
        Log($"ERROR: {message}");
    }
}

class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[LOG] {message}");
    }
    // LogError is inherited from the interface — no need to implement it!
}

Wait — didn't we just say interfaces can't have implementations? Welcome to default interface methods (DIMs). They let you add new methods to an existing interface without breaking every class that already implements it. It's an evolution mechanism.

Important caveat: Default methods are only accessible through the interface type, not the class type:

ConsoleLogger logger = new ConsoleLogger();
// logger.LogError("Oops"); // ❌ Compiler error!

ILogger iLogger = logger;
iLogger.LogError("Oops"); // ✅ Works through the interface

This is a deliberate design choice. Default interface methods are meant for backward compatibility, not for replacing abstract classes.

7. A Taste of IDisposable — Cleaning Up After Yourself

One interface you'll meet constantly in .NET is IDisposable. It has exactly one method:

public interface IDisposable
{
    void Dispose();
}

Classes that hold expensive resources (database connections, file handles, network sockets) implement IDisposable to promise: "Call Dispose() on me and I'll clean up properly."

C# gives you a nice shorthand for this — the using statement:

using StreamReader reader = new StreamReader("data.txt");
string content = reader.ReadToEnd();
Console.WriteLine(content);
// reader.Dispose() is called automatically when 'reader' goes out of scope

No need to call Dispose() manually — the using keyword does it for you. Think of it as a safety net for forgetful developers (which is all of us).

We'll go deeper into IDisposable and resource management later in the series. For now, just know: if a class implements IDisposable, wrap it in using.

8. Your Homework: Build a Notification System

Create a console app with:

  1. An INotificationSender interface with a method void Send(string recipient, string message).
  2. Three classes that implement it: EmailSender, SmsSender, and PushNotificationSender. Each should print a different message to the console.
  3. A method void NotifyAll(List<INotificationSender> senders, string recipient, string message) that loops through all senders and calls Send.
  4. In Main, create a list with all three senders and call NotifyAll.
interface INotificationSender
{
    void Send(string recipient, string message);
}

class EmailSender : INotificationSender
{
    public void Send(string recipient, string message)
    {
        Console.WriteLine($"📧 Email to {recipient}: {message}");
    }
}

class SmsSender : INotificationSender
{
    public void Send(string recipient, string message)
    {
        Console.WriteLine($"📱 SMS to {recipient}: {message}");
    }
}

class PushNotificationSender : INotificationSender
{
    public void Send(string recipient, string message)
    {
        Console.WriteLine($"🔔 Push to {recipient}: {message}");
    }
}

// Usage
List<INotificationSender> senders =
[
    new EmailSender(),
    new SmsSender(),
    new PushNotificationSender()
];

foreach (INotificationSender sender in senders)
{
    sender.Send("Farhad", "Your pizza is ready!");
}

Bonus challenge: Add a fourth sender — DiscordSender — without modifying any existing code. Notice how you just add a new class and drop it in the list? That's the power of interfaces.

Summary of Day 12

  • Interfaces are contracts — they declare what a class can do without specifying how.
  • Interface names start with I by convention (IGreeter, ICanSwim).
  • A class can implement multiple interfaces but inherit from only one base class.
  • Program to an interface, not an implementation — this makes your code flexible and testable.
  • Default interface methods (C# 8+) let you add methods with implementations to interfaces for backward compatibility.
  • IDisposable is the most common .NET interface — use using to auto-call Dispose().
  • Interfaces + polymorphism = code that can handle objects it hasn't even met yet.

Tomorrow: we'll talk about Enums and Structs — lightweight types for when classes feel like overkill. Sometimes you don't need a whole house; a tent will do just fine. ⛺

See you on Day 13!

Share
FM

Farhad Mammadov

.NET Engineer & Cloud Architect · Bayern, Germany. Writing about scalable backend systems, AWS, and SRE.