Object Instantiation
Runtime Typing
Dynamic Typing
Programming
Software Development

Instantiate an object with a runtime-determined type

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Runtime type instantiation is essential for plugin systems, serializers, dependency injection containers, and command dispatch frameworks. The challenge is creating objects safely when concrete type information is not known at compile time. A robust solution balances flexibility with validation, performance, and security.

Reflection-Based Instantiation Basics

In C sharp, you can resolve a type by name and create an instance through Activator.CreateInstance.

csharp
1using System;
2
3public interface ITaskHandler
4{
5    string Execute();
6}
7
8public sealed class EmailTaskHandler : ITaskHandler
9{
10    public string Execute() => "email sent";
11}
12
13public static class Program
14{
15    public static void Main()
16    {
17        string typeName = "EmailTaskHandler";
18        Type? type = Type.GetType(typeName) ?? typeof(Program).Assembly.GetType(typeName);
19
20        if (type is null)
21        {
22            throw new InvalidOperationException("Type not found");
23        }
24
25        object? instance = Activator.CreateInstance(type);
26        var handler = instance as ITaskHandler ?? throw new InvalidOperationException("Invalid handler type");
27
28        Console.WriteLine(handler.Execute());
29    }
30}

This pattern is simple but should always include type checks and controlled type lookup.

Constructor Arguments at Runtime

Many runtime-created types need constructor dependencies. Use overloads of CreateInstance that accept arguments.

csharp
1using System;
2
3public sealed class RetryPolicy
4{
5    private readonly int maxAttempts;
6
7    public RetryPolicy(int maxAttempts)
8    {
9        this.maxAttempts = maxAttempts;
10    }
11
12    public override string ToString() => $"RetryPolicy(maxAttempts={maxAttempts})";
13}
14
15public class Program
16{
17    public static void Main()
18    {
19        Type type = typeof(RetryPolicy);
20        object? instance = Activator.CreateInstance(type, 5);
21        Console.WriteLine(instance);
22    }
23}

If constructor signatures are variable, cache constructor metadata and validate argument count and type upfront.

Prefer Type Registries Over Raw Type Names

Accepting arbitrary type names from configuration can become a security and reliability risk. A safer design maps known keys to known types.

csharp
1using System;
2using System.Collections.Generic;
3
4public interface IPaymentGateway
5{
6    string Name { get; }
7}
8
9public sealed class StripeGateway : IPaymentGateway
10{
11    public string Name => "stripe";
12}
13
14public sealed class PayPalGateway : IPaymentGateway
15{
16    public string Name => "paypal";
17}
18
19public static class GatewayFactory
20{
21    private static readonly Dictionary<string, Type> registry = new(StringComparer.OrdinalIgnoreCase)
22    {
23        ["stripe"] = typeof(StripeGateway),
24        ["paypal"] = typeof(PayPalGateway)
25    };
26
27    public static IPaymentGateway Create(string key)
28    {
29        if (!registry.TryGetValue(key, out var type))
30        {
31            throw new ArgumentException("Unknown gateway", nameof(key));
32        }
33
34        return (IPaymentGateway)Activator.CreateInstance(type)!;
35    }
36}

The registry gives runtime flexibility without exposing unrestricted reflection.

Java Equivalent

Java uses reflection similarly with Class.forName and constructors.

java
1interface Formatter {
2    String format(String input);
3}
4
5class UpperCaseFormatter implements Formatter {
6    public String format(String input) {
7        return input.toUpperCase();
8    }
9}
10
11public class Demo {
12    public static void main(String[] args) throws Exception {
13        String className = "UpperCaseFormatter";
14        Class<?> clazz = Class.forName(className);
15
16        Object instance = clazz.getDeclaredConstructor().newInstance();
17        Formatter formatter = (Formatter) instance;
18
19        System.out.println(formatter.format("runtime"));
20    }
21}

The same discipline applies: whitelist known classes and validate interfaces before casting.

Performance and Caching

Reflection has overhead. If runtime instantiation occurs frequently, compile expression-based factories or cache constructor delegates.

csharp
1using System;
2using System.Collections.Concurrent;
3using System.Linq.Expressions;
4
5public static class FastFactory {
6    private static readonly ConcurrentDictionary<Type, Func<object>> cache = new();
7
8    public static object Create(Type type) {
9        var creator = cache.GetOrAdd(type, t => {
10            var ctor = Expression.New(t);
11            var lambda = Expression.Lambda<Func<object>>(Expression.Convert(ctor, typeof(object)));
12            return lambda.Compile();
13        });
14
15        return creator();
16    }
17}

Caching can significantly reduce repeated reflection cost in high-throughput systems.

Integrating with Dependency Injection

In application code, prefer resolving runtime keys to service registrations rather than calling reflection directly in business paths. Most dependency injection containers already map abstractions to concrete implementations and handle constructor dependency graphs.

A practical pattern is converting runtime input into a known key, validating it against a registry, then asking the container for that concrete type. This keeps object creation policy centralized and easier to test.

Common Pitfalls

  • Creating types from untrusted raw strings. Fix by using whitelisted registries.
  • Skipping interface validation after instantiation. Fix by casting and failing fast with explicit errors.
  • Ignoring constructor mismatch errors. Fix by validating signature expectations before invoking constructors.
  • Using reflection in hot loops with no cache. Fix by caching constructors or compiled factory delegates.
  • Mixing object creation with business logic. Fix by isolating runtime instantiation into dedicated factory components.

Summary

  • Runtime type instantiation is useful but needs strict guardrails.
  • Reflection APIs can create objects dynamically when paired with validation.
  • Registries are safer than arbitrary type-name loading.
  • Constructor caching improves performance in frequent creation paths.
  • Encapsulate dynamic creation in factory layers for maintainable design.

Course illustration
Course illustration

All Rights Reserved.