C# const vs readonly — Interview Questions

Q1. What’s the difference between const and readonly?

const is a compile-time constant and is implicitly static. readonly is assigned at runtime during initialization or construction and cannot be reassigned afterward.

Why?

const represents a value the compiler can know; readonly represents runtime state that becomes fixed after initialization.

Q2. Why not just use private?

private controls accessibility; it doesn’t prevent reassignment inside the class.

private string _name;          // can change internally
private readonly string _name; // cannot reassign after construction

Why?

private and readonly solve different problems: access control vs mutation control.

Q3. What’s the Java equivalent of readonly?

Java’s closest equivalent is final for fields.

private final String name;
private readonly string _name;

Why?

Both allow initialization followed by no reassignment.

Q4. Can a readonly field have a setter?

No. A property setter cannot bypass the readonly restriction.

If you want:

Outside → READ
Class   → WRITE

use:

public string Name { get; private set; }

Why?

private set controls property access, whereas readonly controls field reassignment.

Q5. Can get; private set; change after construction?

Yes, from inside the class.

public string Status { get; private set; }
 
public void Complete()
{
    Status = "Completed"; // ✅
}

Why?

private set allows the containing class to modify the property.

Q6. Can a readonly field reference a mutable object?

Yes.

private readonly List<string> _items = new();
 
_items.Add("A"); // ✅
_items = new();  // ❌

Why?

readonly prevents changing the reference, not changing the referenced object’s contents.

Q7. When would you use static readonly instead of const?

When there should be one value for the type, but the value must be calculated or obtained at runtime.

public static readonly Guid ApplicationId = Guid.NewGuid();

Why?

Guid.NewGuid() cannot be evaluated at compile time.

Q8. Give the Java → C# mapping.

Java                              C#
 
private final String name    →    private readonly string _name;
 
static final int MAX = 10    →    const int Max = 10;
 
static final Guid id =       →    static readonly Guid Id =
    UUID.randomUUID();            Guid.NewGuid();

Why?

The first is write-once instance state, the second is a compile-time constant, and the third is a runtime-initialized class-level value.

30-second answer

private controls who can access a field, while readonly controls whether the field can be reassigned. readonly is roughly analogous to Java’s final for fields. const is different because it is a compile-time constant and is implicitly static. I use const for genuine compile-time invariants, readonly for runtime values that should be fixed after construction, and get; private set; when the class needs controlled mutation.”


Quick Interview Prep: Rapid-Fire C# & .NET Core Essentials

Since your interview is in a few hours, let’s calm those nerves and hit the absolute highest-yield topics interviewers love to grill developers on. Here is your fast-track cheat sheet:

1. Value Types vs. Reference Types

  • Value Types: Stored on the stack (or inline inside structures). They hold the actual data directly (e.g., int, bool, struct, enum). When copied, a full copy of the data is made.

  • Reference Types: Stored on the heap. The variable stores a reference (memory address) pointing to the actual data (e.g., class, string, array, interface). When assigned or passed, only the reference address is copied, meaning two variables can point to the same object.

2. What is async / await and How Does It Work?

  • Interviewers love this: Async/await does not create a new thread.

  • Instead, it enables non-blocking I/O. When your code hits an await keyword (like querying a database or calling an external API), the current thread is released back to the thread pool to handle other requests. Once the I/O operation finishes, a thread picks up the continuation of the method.

3. Dependency Injection (DI) Lifetimes in .NET Core

You must know these three lifetimes by heart:

  • Transient (AddTransient): Created every single time they are requested. Best for lightweight, stateless services.

  • Scoped (AddScoped): Created once per client request (HTTP request in web apps). Best for things like Entity Framework DbContext.

  • Singleton (AddSingleton): Created the first time they are requested, and that exact same instance is used forever across the entire application lifecycle. Best for caching or configuration settings.

4. Garbage Collection (GC) in .NET

  • Managed memory is split into three Generations (Gen 0, Gen 1, Gen 2) based on object lifespan.

  • Gen 0: Short-lived objects (like local variables). Collected frequently and very fast.

  • Gen 1: A buffer zone between short and long-lived objects.

  • Gen 2: Long-lived objects (like static variables or singletons). Collected rarely.

  • Key takeaway: You don’t manually free memory in C#; the GC handles it, but you should still implement IDisposable to clean up unmanaged resources (like file handles or database connections).


1. Production-Standard Architecture Overview

In an interview, architecture matters as much as code. A production-ready .NET 8 Web API follows Clean Architecture / Layered Design combined with SOLID principles:

  • Controllers / Endpoints: Handle HTTP requests and responses (Single Responsibility Principle).

  • Service Layer: Houses core business logic, adhering to interfaces (Dependency Inversion Principle).

  • Infrastructure Layer: Handles database context, external API calls, and message brokers like Kafka.

  • Middleware: Intercepts the HTTP pipeline for cross-cutting concerns (authentication, global error handling, request logging).

2. Implementing OOP Principles in the API

Interviewers love seeing how you apply OOP beyond textbook definitions. Here is how they map to our API structure:

  1. Encapsulation: Entities and DTOs hide their internal state and expose controlled behavior. Domain models use private setters or init-only properties (public string Id { get; init; }).

  2. Inheritance & Polymorphism: Used in domain services or message handlers (e.g., an abstract base producer class inherited by specific Kafka event producers).

  3. Abstraction & Interfaces: Every service (IUserService, IKafkaProducerService) is injected via interfaces, enabling unit testing via mocking and decoupling layers.

3. Custom Middleware Implementation

Middleware forms a pipeline that handles incoming requests and outgoing responses. Here is a production-grade custom middleware for Global Exception Handling and Request Timing (Observability), formatted cleanly for your notes:

C#

using System.Diagnostics;
using System.Net;
using System.Text.Json;

namespace HelloCS.Middleware;

public class PerformanceAndExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<PerformanceAndExceptionMiddleware> _logger;

    public PerformanceAndExceptionMiddleware(RequestDelegate next, ILogger<PerformanceAndExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            // Call the next middleware in the pipeline
            await _next(context);
            
            stopwatch.Stop();
            if (stopwatch.ElapsedMilliseconds > 500)
            {
                _logger.LogWarning("Performance Warning: Request {Path} took {ElapsedMs}ms", 
                    context.Request.Path, stopwatch.ElapsedMilliseconds);
            }
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            _logger.LogError(ex, "Unhandled exception occurred during request {Path}", context.Request.Path);
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        var response = new 
        {
            StatusCode = context.Response.StatusCode,
            Message = "An internal server error occurred.",
            Detailed = exception.Message // In production, hide detailed exceptions from clients
        };

        return context.Response.WriteAsync(JsonSerializer.Serialize(response));
    }
}

// Extension method for clean registration in Program.cs
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UsePerformanceAndExceptionHandling(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<PerformanceAndExceptionMiddleware>();
    }
}

4. Kafka Integration Pattern (.NET 8)

For high-throughput architecture, Kafka is usually integrated using the popular Confluent.Kafka NuGet package wrapped inside a background service or scoped producer service.

C#

using Confluent.Kafka;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace HelloCS.Messaging;

public interface IEventProducer
{
    Task PublishAsync(string topic, string messageKey, string messageValue);
}

public class KafkaEventProducer : IEventProducer, IDisposable
{
    private readonly IProducer<string, string> _producer;
    private readonly ILogger<KafkaEventProducer> _logger;

    public KafkaEventProducer(IConfiguration configuration, ILogger<KafkaEventProducer> logger)
    {
        _logger = logger;
        var config = new ProducerConfig
        {
            BootstrapServers = configuration["Kafka:BootstrapServers"] ?? "localhost:9092"
        };
        _producer = new ProducerBuilder<string, string>(config).Build();
    }

    public async Task PublishAsync(string topic, string messageKey, string messageValue)
    {
        try
        {
            var result = await _producer.ProduceAsync(topic, new Message<string, string>
            {
                Key = messageKey,
                Value = messageValue
            });
            
            _logger.LogInformation("Delivered Kafka message to {TopicPartitionOffset}", result.TopicPartitionOffset);
        }
        catch (ProduceException<string, string> ex)
        {
            _logger.LogError(ex, "Failed to deliver Kafka message to topic {Topic}", topic);
            throw;
        }
    }

    public void Dispose()
    {
        _producer?.Flush(TimeSpan.FromSeconds(5));
        _producer?.Dispose();
    }
}

5. Top Interview Questions & Answers

When the interviewer drills down into these concepts, use these concise, high-impact explanations:

  • Q: What is the difference between Task.Run and Task.Yield in async programming?

    • Answer: Task.Run queues work items to the Thread Pool to offload CPU-bound work from the calling thread. Task.Yield is an asynchronous way to yield execution back to the caller immediately, allowing the current synchronization context or thread to handle other queued work before resuming.
  • Q: How does dependency injection manage thread safety with singletons?

    • Answer: Because Singleton instances live for the entire application lifetime and can be accessed concurrently by multiple HTTP request threads, singletons must be thread-safe. You should avoid storing mutable request-specific state inside singleton services; instead, use thread-safe collections (ConcurrentDictionary) or stateless design patterns.
  • Q: Explain how Kafka partitions ensure scalability and ordering.

    • Answer: A Kafka topic is split into partitions. Messages with the same key always land on the same partition, guaranteeing strict ordered processing per key. Multiple consumers in a consumer group read from separate partitions in parallel, enabling horizontal scalability.

Would you like me to map out how to bundle all of this into a complete Program.cs file structure next?