DEV Community

Designing a scalable application with interfaces in C#

The PandApache3 web server is based on a modular architecture. But what does that mean in really?


The modules of a web server

If we consider the PandApache3 service as a "black box," we can still break it down into three main modules:

  • Web Module: Handles HTTP requests on port 80, providing access to the site hosted by PandApache3.
  • Admin Module: Provides a web interface for administering the PandApache service.
  • Telemetry Module: Used to collect and analyze data on the service's performance.

These modules work in synergy but can also be run independently, which is very convenient during development. For example, you can activate only the telemetry module for testing without being bothered by the other modules. This flexibility simplifies testing and speeds up development cycles.

This architecture also makes future improvements much easier. For instance, if I want to add a new feature to the server, I just need to write a new module. Thanks to this approach, the new module can easily integrate into the rest of the code without disrupting existing modules.


Simplified integration with interfaces

To standardize each module's behavior, PandApache3 uses a C# interface called IModule. Let’s look at what an interface brings in practice.

An interface in C# is a contract that defines a series of methods without implementing them. By imposing a common framework, it ensures that all modules have the same basic functionality while allowing each module to customize its behavior.

Here is the IModule interface used in PandApache3:

public interface IModule
{
    Task StartAsync();
    Task RunAsync();
    Task StopAsync();
    bool isEnable();
}
Enter fullscreen mode Exit fullscreen mode

Each module must be able to start, run, and stop. The isEnable() method checks if the module is activated based on the service configuration. By implementing this interface, each module ensures it adheres to this structure.

Example implementation for the telemetry module:

public class TelemetryModule : IModule
{
    public async Task StartAsync()
    {
        // Initialize data collection
    }

    public async Task RunAsync()
    {
        // Continuously collect telemetry metrics
    }

    public async Task StopAsync()
    {
        // Stop data collection
    }

    public bool isEnable()
    {
        // Check if the module is enabled
        return ModuleInfo.isEnable;
    }
}
Enter fullscreen mode Exit fullscreen mode

The telemetry module, like the web or admin module, uses this interface to implement its own features.


Using polymorphism

Now that we’ve defined an interface and classes that implement it, how do we manage them uniformly? This is where polymorphism comes in.

Polymorphism is a programming concept that allows different classes to be used in the same way. By using the IModule interface, PandApache3 can handle all modules uniformly, regardless of their actual type.

In the main server class, modules are initialized and then stored in a dictionary of type IModule:

public Dictionary<ModuleType, IModule> Modules = new Dictionary<ModuleType, IModule>();

// Module initialization
TelemetryModule telemetryModule = new TelemetryModule(telemetryTaskScheduler);
ConnectionManagerModule webModule = new ConnectionManagerModule(ModuleType.Web, Pipelines["web"], webTaskScheduler);
ConnectionManagerModule adminModule = new ConnectionManagerModule(ModuleType.Admin, Pipelines["admin"], adminTaskScheduler);

Modules.Add(ModuleType.Telemetry, telemetryModule);
Modules.Add(ModuleType.Web, webModule);
Modules.Add(ModuleType.Admin, adminModule);

// Keep only the modules enabled in the configuration
foreach (var moduleKey in Modules.Keys.ToList())
{
    if (!Modules[moduleKey].isEnable())
    {
        ExecutionContext.Current.Logger.LogWarning($"Module {moduleKey} disabled");
        Modules.Remove(moduleKey);
    }
}
Enter fullscreen mode Exit fullscreen mode

Although the modules are created as TelemetryModule or ConnectionManagerModule objects, they are stored as IModule objects. This way, we can manage them uniformly without needing to know their specific types.


Starting and running modules

The server uses a loop to start all the activated modules, thanks to polymorphism. Here’s what it looks like:

foreach (var moduleName in Modules.Keys)
{
    await Modules[moduleName].StartAsync();
}
Enter fullscreen mode Exit fullscreen mode

Using IModule allows starting, running, and stopping each module in the same loop, regardless of the module’s type. For example, to run all the modules:

List<Task> tasks = new List<Task>();
foreach (var moduleName in Modules.Keys)
{
    tasks.Add(Task.Run(() => Modules[moduleName].RunAsync()));
}

await Task.WhenAll(tasks);
Enter fullscreen mode Exit fullscreen mode

And to stop them:

foreach (var moduleName in Modules.Keys)
{
    await Modules[moduleName].StopAsync();
}
Enter fullscreen mode Exit fullscreen mode

Adding a new module

Thanks to this modular architecture, adding a new module to PandApache3 is simple: just write a new class that implements IModule. The interface sets the framework, ensuring easy and consistent integration.

Interfaces play a crucial role throughout PandApache3. For example, all middlewares follow the IMiddleware interface, allowing them to run in sequence. Components such as the socket, logger, file manager, and even configuration manager also use interfaces, making the code more flexible and easier to test.

With this modular approach and intelligent use of interfaces, PandApache3 becomes an extensible and maintainable web server, ready to evolve with new features without requiring significant changes to the existing codebase.


I hope this article helped you better understand the concrete and essential role of interfaces in C#. If you’re interested in this language, know that the PandApache3 code is available on GitHub and live on Twitch. Feel free to follow the journey!

Top comments (0)