
This is one of those questions that, when asked, receives a bunch of different answers, and I am glad to read all of them in the comment section. You are all invited to give your opinion, and we can discuss it further there.
So, what is a Senior Developer? Based on my 12 years of professional experience, seniority comes down to understanding the fundamentals. It’s really that simple and that hard at the same time.
When we first dive into programming and start learning the very basics, we stumble upon variables, classes, interfaces, and basic concepts such as inheritance and polymorphism. After practicing and using them over the next few years, we believe that we have mastered those concepts, that we now understand what they are — only to find out later that no, we had it all wrong.
Let’s take a look at some examples to better understand this post. Let’s start with the fluid of every piece of code: the variable.
Variable
Let’s illustrate this with something that we all use daily: a price. Programming languages provide two data types that can represent a price — int
and float
. Most developers will use the float
data type to represent a price, in order to handle cases with decimals, such as $1.47. It sounds logical at first, only to realize later that an int
might be a better type. Instead of saving the price as 1.47, we can save it as 147. That number now represents cents, and suddenly the declaration becomes more readable and simpler to use. Even if you want to display the price as 1.47, you can simply create a method that divides 147 by 100.
Years pass by, you gain more experience, and you start learning about value objects. You are amazed by them because an int
or a float
doesn’t make sense anymore to represent a price—you want something more. You want a price to be represented… by a price. So you go and create a Price
class that accepts both a currency and a value.
class Price
{
public function __construct(
public readonly int $value,
public readonly string $currency,
) {
if ($value < 0) {
throw new InvalidArgumentException('Value must be non-negative.');
}
if (!is_string($currency) || strlen($currency) !== 3) {
throw new InvalidArgumentException('Currency must be a 3-letter ISO 4217 code (e.g., \\'USD\\').');
}
}
public function __toString(): string
{
return sprintf('%s %.2f', $this->currency, $this->value / 100);
}
}
Now it all makes sense. A price isn’t an int
or a float
; a price is a price. But it took you years to understand this basic concept. Representing a price as an int
or float
is the easiest option, and we humans are wired to find the easiest path to solve a problem. But that doesn’t mean it’s the best path.
Wasn’t that beautiful? With a couple of words, we traveled through 5–7 years of software development. We started in year 1 with the basics and achieved seniority, where we made use of the fundamentals to create a higher abstraction.
Programming is indeed a state of the art.
Class/Object
When we first learn about classes, we think of them as containers for related variables and methods. A User
class holds user data and user-related functions. Simple enough. As we gain experience, we realize classes aren't just containers - they're abstractions that represent concepts in our problem domain.
After a few years, we discover the true power of object-oriented principles. We learn that a class should have a single responsibility, that it should encapsulate its data, and that its public interface should be carefully designed. Let’s look at an evolution:
// Beginner approach - class as a data container
class User
{
public function __construct(
public string $name,
public string $email,
public string $password, // Storing password directly - bad!
) {}
}
// Intermediate approach - adding validation and encapsulation
class User
{
private string $name;
private string $email;
private string $passwordHash;
public function __construct(string $name, string $email, string $password)
{
$this->name = $name;
$this->email = $this->validateEmail($email);
$this->passwordHash = password_hash($password, PASSWORD_ARGON2ID);
}
public function getName(): string
{
return $this->name;
}
...
private function validateEmail(string $email): string
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}
return $email;
}
}
// Senior approach - rich domain model
final class Email
{
public function __construct(public readonly string $value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}
}
}
final class Password
{
private string $hash;
public function __construct(string $plaintext)
{
$this->hash = password_hash($plaintext, PASSWORD_ARGON2ID);
}
public function matches(string $attempt): bool
{
return password_verify($attempt, $this->hash);
}
}
final class User
{
private int $loginAttempts = 0;
private bool $isLocked = false;
public function __construct(
private readonly string $name,
private readonly Email $email,
private readonly Password $password,
) {}
public function authenticate(string $attempt): bool
{
if ($this->isLocked) {
throw new AccountLockedException('Account is locked due to too many failed attempts');
}
if (!$this->password->matches($attempt)) {
$this->loginAttempts++;
if ($this->loginAttempts >= 3) {
$this->isLocked = true;
}
return false;
}
$this->loginAttempts = 0;
return true;
}
}
Interface
Initially, we see interfaces as abstract contracts that classes must implement. But with experience, we realize they’re much more — they’re boundaries between different parts of our system, they’re promises about behavior, and they’re tools for dependency inversion.
// Beginner approach - interface as a contract
interface PaymentProcessor
{
public function processPayment(float $amount): bool;
}
// Senior approach - interface as a boundary
final class PaymentResult
{
public function __construct(
public readonly bool $success,
public readonly ?string $transactionId = null,
public readonly ?string $error = null,
) {}
}
final class PaymentContext
{
public function __construct(
public readonly Price $amount,
public readonly Customer $customer,
public readonly array $metadata,
) {}
}
interface PaymentProcessor
{
public function processPayment(PaymentContext $context): PaymentResult;
}
Inheritance
Perhaps no concept evolves more dramatically than inheritance. We start by seeing it as a way to reuse code — if two classes share properties, make one inherit from the other! Years later, we realize this leads to tight coupling and brittle hierarchies. We learn about composition over inheritance, and our understanding shifts entirely:
// Beginner approach - inheritance for code reuse
abstract class Animal
{
public function __construct(protected string $name) {}
abstract public function makeSound(): string;
}
class Dog extends Animal
{
public function makeSound(): string
{
return 'Woof!';
}
}
// Senior approach - composition and behavior delegation
final class Sound
{
public function __construct(private readonly string $sound) {}
public function make(): string
{
return $this->sound;
}
}
final class Animal
{
public function __construct(
private readonly string $name,
private readonly Sound $soundMaker,
) {}
public function makeSound(): string
{
return $this->soundMaker->make();
}
}
$dog = new Animal('Rex', new Sound('Woof!'));
The senior approach shows understanding that:
- Inheritance creates tight coupling between classes
- Composition allows for more flexible and maintainable code
- Behavior should be encapsulated in dedicated classes
- Dependencies should be injected rather than hardcoded
This evolution in understanding fundamentals is what marks the journey to senior development. It’s not about knowing more syntax or frameworks — it’s about deeper comprehension of these basic concepts and how to apply them effectively to create maintainable, robust software.
— Are you looking 👀 for a developer❓ I’m available for hire, contact me at renato@hysa.dev —
Let’s turn your idea to reality. Fast, secure, and within a budget.