DTOs Are Powerful.

Whether you're in C#, Python, Java, PHP, TypeScript, or Ruby, somewhere in your codebase there's a dictionary getting passed around like a mystery grab bag at a white elephant party. "Here's some stuff. Figure it out."

Look familiar?

public void ProcessUser(IDictionary<string, object> userData)
{
    string name = (string)userData["name"];
    int age = (int)userData["age"];
}

def process_user(user_data: dict):
    name = user_data['name']
    age = user_data['age']

public void processUser(Map<String, Object> userData) {
    String name = (String) userData.get("name");
    int age = (int) userData.get("age");
}

function processUser(array $userData) {
    $name = $userData['name'];
    $age = $userData['age'];
}

function processUser(userData: { [key: string]: any }) {
    const name = userData['name'] as string;
    const age = userData['age'] as number;
}

def process_user(user_data)
  name = user_data[:name]
  age = user_data[:age]
end

Just use DTOs.


public class UserDto
{
    public string Name { get; init; }
    public int Age { get; init; }
}

public void ProcessUser(UserDto user)
{
    string name = user.Name;
    int age = user.Age;
}


@dataclass(frozen=True)
class UserDto:
    name: str
    age: int

def process_user(user: UserDto):
    name = user.name
    age = user.age
    
    
public record UserDto(String name, int age) {}

public void processUser(UserDto user) {
    String name = user.name();
    int age = user.age();
}

readonly class UserDto {
    public function __construct(
        public readonly string $name,
        public readonly int $age
    ) {}
}

function processUser(UserDto $user) {
    $name = $user->name;
    $age = $user->age;
}

interface UserDto {
    readonly name: string;
    readonly age: number;
}

function processUser(user: UserDto) {
    const name = user.name;
    const age = user.age;
}

UserDto = Struct.new(:name, :age, keyword_init: true) do
  def initialize(name:, age:)
    super
    freeze
  end
end

def process_user(user)
  name = user.name
  age = user.age
end

They're a shape. A contract. "This is what crosses this boundary, and it looks like this." Make them immutable. If you're mutating a DTO somewhere in the pipeline, you don't have a DTO you have a ... gremlin or post-it note.

The real win: when you add a field, you change the DTO, and the compiler yells at every place that needs to care. The dictionary approach? Silent failures. Misspelled keys. Runtime surprises. You're debugging at 2am because someone fat-fingered "userID" as "userId" three sprints ago.

Stop passing bags of maybes. Define and enforce the shape. Move on with your life.

❤️
Jake