megacolorboy

Abdush Shakoor's Weblog

Writings, experiments & ideas.

Things I Learned as a Senior Engineer

I've spent the last 10 years working as a professional programmer and these are some of the lessons I have learned along the way.

Career Growth

  • If you want to grow in your career, don't stay in your comfort zone for too long. Changing companies can expose you to new environments, new challenges, new people, and new ways of thinking.
  • It's good to be smart, but if you ever become the smartest person in the room, it may be time to look for another opportunity.
  • Work with smart engineers. They will make you a better programmer.
  • Work with smart non-technical people too. They will make you a better engineer.
  • Make good money, but save it too.
  • Time is finite, so spend it carefully. Play with your wife and kids, talk to your friends, watch a good movie, read a book, work out, pray, or take a long walk. Work is not everything.

Engineering Fundamentals

  • It doesn't matter what tech stack is popular right now. Get good at the fundamentals and the rest will follow.
  • When I was a junior engineer, I cared about knowing the next big thing. These days, I care more about understanding the basics deeply.
  • Learn databases, especially SQL. It is one of those skills that will save you in many situations.
  • Invest in networking, databases, system design, data engineering, computer science concepts, and distributed systems.
  • Want to understand operating systems better? Learn Linux and use it as your everyday OS. You will need that knowledge more than you think.
  • Tech keeps changing, so keep yourself updated through good sources like Hacker News.
  • Adapt to current tech trends, but don't blindly chase everything. Sometimes you need exposure to different parts of the software development cycle.

Code And Craft

  • The best code is often the code you don't have to write. Remove complexity before adding more code.
  • A good engineer knows best practices. A great engineer knows when to break them.
  • Don't worry about writing bad code sometimes. Everyone does it. Myself included.
  • The older I get, the more I appreciate strongly typed languages. At the very least, I try to bring some of that discipline into dynamic languages too.
  • Version your code. If you don't know how to use version control, learn it. If you choose not to, stop and fix that first.
  • Document what you do. I learned this the hard way. Good documentation, shared with the team, can save you from unnecessary calls, interruptions, and spoiled vacations.
  • I've never been a strict TDD person, but these days I try to write more tests. Good tests are useful when they protect behavior that matters.
  • I'm not a security expert, but I know enough to take security seriously. Security is not a joke.
  • Don't rely too much on Claude, ChatGPT, or any other AI model. They are useful tools, but they should not replace your own thinking.

People And Teams

  • I have made good, lifelong relationships at companies I have worked with. I try to keep work relationships professional, but we are human, and sometimes real friendships happen.
  • Stay away from office politics. Don't insert yourself into other people's business. It will save you from a lot of trouble.
  • Be honest and transparent with your line manager and management. You can decide how much to share, but being authentic usually goes a long way.
  • A good manager shares many qualities with a good engineer: clarity, consistency, judgment, and the ability to unblock people.
  • Be kind and helpful to your team whenever you get the chance. It goes a long way, and sometimes someone will help you when you are stuck too.
  • Work with like-minded people, especially in software engineering. It makes the work better.
  • Don't judge engineers only by title or years of experience. Watch how they debug, read unfamiliar code, and reason through trade-offs.

Work Environment

  • It is okay to deal with emergencies and late-night operations occasionally. If it becomes normal and starts harming your mental health, stop and find something that works better for you.
  • I am not a fan of working from home full-time. I prefer keeping work separate from home because it keeps things organized for me.
  • Don't work on bad hardware if you can avoid it. Every developer deserves good tools.
  • Full-stack developers deserve more respect. They are expected to understand frontend, backend, databases, networking, browsers, caching, mobile behavior, and more.
  • Keep healthy boundaries at work. No one needs access to your full personal life or your most private self.

Life Outside Work

  • If you have time, work on side projects. Tinker, build things you care about, and stay curious.
  • I got into tech and programming because it was my hobby. Sometimes your work can be the same as your hobby, and sometimes work can ruin the hobby for you. Learn new hobbies whenever you can.
  • Mental health is important. If you are constantly working late nights and committing to unreasonable timelines, pu sh back. If that does not work, find something better for you.

That's the list for now. I will probably disagree with parts of it in a few years, and that is part of the job too.

Enable password asterisks for sudo on Fedora

After switching from Linux Mint to Fedora, one small difference I noticed was the password prompt behavior in the terminal. On Mint, sudo showed asterisks while typing the password. On Fedora, it stayed blank.

Some people prefer the blank prompt, but I like having visual feedback when typing, especially if I am not fully sure whether I missed a key.

If you want the same behavior on Fedora, you can enable sudo password feedback with a small sudoers change.

This note applies to sudo prompts in the terminal, not graphical login screens.

1. Open the sudoers file safely

Do not edit /etc/sudoers directly with a normal text editor. Use visudo so syntax errors are caught before the file is saved:

sudo visudo

2. Enable password feedback

Add this line:

Defaults pwfeedback

If you already have other Defaults lines such as Defaults env_reset, leave them as they are. Just add Defaults pwfeedback as a separate line.

3. Save and test

Save the file and exit the editor. Then run a sudo command again, for example:

sudo -k
sudo true

When prompted for your password, you should now see asterisks as you type. This setting makes password length visible on screen. If that matters in your environment, leave the default behavior in place.

Hope you found this article useful!

Clean removal of Linux from a Windows dual-boot machine

Recently, I purchased a ThinkPad X1 Carbon Gen 13 to replace my old Dell XPS 13 9360. It came with Windows 11, and instead of wiping it, I decided to set up a dual-boot system with Linux. I still need Windows for .NET development, but I use Linux most of the time.

I eventually settled on Fedora, but I tried several distributions and desktop environments before getting there. Each time I removed Linux, I needed to clean up the old partitions and boot entries properly so they would not interfere with the next installation.

If you are doing the same kind of distro-hopping on a UEFI system, this is the cleanup process that worked for me.

This note covers UEFI systems only.

If you want to remove Linux from a Windows dual-boot setup, there are two things to clean up:

  1. Delete the Linux partitions.
  2. Remove the Linux bootloader files and firmware entries.

Before making changes, make sure Windows is working normally and back up anything you want to keep from your Linux installation.

1. Delete the Linux partitions

Open Windows Disk Management and identify the partitions that belong to Linux. These are usually the Linux root partition, swap partition, and optionally a separate /home partition.

Do not delete:

  • The Windows partition
  • The EFI System Partition
  • The Windows recovery partition

Delete only the Linux partitions, then either leave the space unallocated or extend your Windows partition into it.

2. Remove Linux bootloader files from the EFI partition

Open Command Prompt as Administrator and assign a drive letter to the EFI System Partition:

diskpart
list vol
select vol <EFI volume number>
assign letter=S
exit

Then remove the Linux bootloader directories from the EFI partition:

S:
cd EFI
dir
rmdir /s /q <linux-folder>

Only delete the directory that matches your Linux installation. Common names include fedora, ubuntu, debian, and similar distro-specific folders.

Do not delete Microsoft.

3. Remove stale UEFI firmware boot entries

List the firmware boot entries:

bcdedit /enum firmware

Look for entries that reference your removed Linux installation, such as descriptions containing Fedora, ubuntu, GRUB, or a path that points to the distro's EFI loader.

Delete only the matching Linux entry:

bcdedit /delete {xxxx-xxxx-xxxx-xxxx}

Replace {xxxx-xxxx-xxxx-xxxx} with the actual identifier from the previous command.

4. Reboot and verify

Reboot the machine and confirm that the Linux boot entry is gone from the firmware boot menu and that Windows starts normally.

Hope you found this article useful!

Rewriting my SSG again — The right way

Around eight years ago, I wrote my own static site generator in Python. It was very simple. No frameworks, no strong structure — just scripts that generated pages for my blog.

It worked, so I kept using it.

There were no proper validations. No structured exception handling. Almost no separation of concerns. I didn’t follow standard practices. At that time, it was just a hobby project, and I only cared that it produced HTML files correctly.

And it did.

To be honest, I was too lazy to rewrite it and it was "stable" enough, so I ignored the technical debt.

Contrasting differences

Over the years, I’ve worked on much larger and more structured systems — enterprise APIs, background services, integrations, layered architectures. I’ve learned to care about validation, clean boundaries, proper error handling, and maintainability.

Now when I open my old SSG code, I can clearly see the difference.

It reflects how I used to think about code.

It’s not terrible. It’s just basic. It lacks discipline.

There’s no validation layer. No clear contracts. Some modules handle too many responsibilities. Dependencies are outdated. Type hints are either missing or inconsistent.

The real issue is that it works — but extending it doesn’t feel comfortable.

Why I decided to rewrite it?

This rewrite is not about fixing bugs. There are no critical issues. It’s just that single god-like python class is really bugging me.

I want to:

  • Add proper validation
  • Introduce structured exception handling
  • Strengthen type hints
  • Upgrade dependencies
  • Separate responsibilities clearly
  • Make it easier to extend in the future

I also want to refresh my blog properly. If I’m going to continue building on top of this tool, it should be something I trust and feel comfortable maintaining.

Right now, adding a new feature feels like touching something fragile. After refactoring, I want it to feel stable and predictable.

Time for some justice!

This time, I’m approaching it like a real project, even though it’s still personal.

Clear module boundaries. Explicit data models. Proper validation. Clean CLI entry points. Better organization overall.

I’ll be using PyCharm heavily during this process. When renaming models, reorganizing modules, or tightening type contracts, I want safe refactoring and immediate feedback. Strong inspections and accurate find usages will help a lot when reshaping older code.

Tooling becomes more important during refactoring than during initial development.

I’m not trying to over-engineer it. I’m just trying to build it properly — based on what I’ve learned over the years.

Conclusion

Sometimes old code is not a mistake. It’s just a snapshot of your earlier experience.

Rewriting this SSG is simply updating it to match how I think today.

Over the next few days, I’ll be cleaning it up and rebuilding it with better structure. Not because it failed — but because I’ve improved.

Hope you liked reading this article!

How JetBrains IDEs Improved My Productivity Across .NET, Laravel, and Python

I work across three different stacks almost every week: .NET for enterprise APIs and integrations, Laravel for large backend systems, and Python for CLI tools and automation.

On paper, these ecosystems are very different. Different communities, different tooling culture, different philosophies.

But my development experience feels almost the same every day — because I use JetBrains IDEs for all of them.

Rider, PHPStorm, and PyCharm share almost identical core capabilities:

  • Smart navigation
  • Safe refactoring
  • Powerful debugging
  • Git integration
  • Database tools
  • Deep code inspections

This is not about which IDE has more features, it’s about staying in that flow of productivity.

Being in the flow

What I like about the JetBrains suite is that, when I move from .NET to Laravel to Python, I don’t feel like I switched tools. I only switched languages and frameworks.

That small difference matters more than people think.

As developers, we already deal with architecture decisions, business rules, integrations, performance issues, and production risks. When I change stacks, I don’t want to also change keyboard shortcuts, debugger behavior, navigation style, or refactoring workflow.

With JetBrains, the mental model stays stable and reduces my mental friction in a very real way.

No more refactoring nightmares!

I might be slightly opinionated but hear me out, okay?

If you work on serious systems — payments, integrations, background jobs, multi-layer architectures — you cannot afford sloppy refactoring.

I frequently:

  • Move logic from controllers into services
  • Rename DTO properties
  • Break large files into smaller components
  • Extract interfaces
  • Clean legacy code

Making use of features like: Search Everywhere, Go to definition, Find usages or Refactor has significantly improved my code confidence and that allows me to continuously improve architecture instead of being afraid to touch old code.

Debugging feels calm and predictable

Whether I’m debugging:

  • A controller in .NET
  • A service in Laravel
  • A module in Python

The experience is consistent and predictable: breakpoints, variable inspection, stepping into async calls, evaluating expressions, you name it!

When debugging feels predictable, solving complex problems becomes less stressful. And that directly improves productivity.

The IDE as a Second Reviewer

Another thing I value is how deeply the IDE understands the code.

It doesn’t just highlight syntax errors. It understands type relationships, method references, incorrect imports, potential null issues, and unused dependencies.

Many mistakes are caught before I even run the application.

It feels like having a second reviewer sitting beside me while I write code.

For someone working across multiple stacks, that safety net is quite powerful.

A consistent ecosystem

The biggest advantage, however, is consistency.

I don’t want three different mental environments. Rather something that adapts to the language. With Rider, PHPStorm, and PyCharm: The engine, navigation and philosophy feels the same.

I mean, don't get me wrong, I still use VS Code, Visual Studio, and lighter setups. They are really good tools in their own way.

But when working on enterprise APIs, government systems, payment workflows, and long-running background processes, I prefer:

  • Depth over minimalism
  • Strong refactoring over quick editing
  • Intelligent tooling over lightweight flexibility

Productivity is rarely about one big feature. It’s about small improvements repeated every day:

  • Strong Git integration
  • Built-in database tools
  • Consistent formatting
  • Reliable search
  • Clean UI scaling on high-resolution screens

Each one saves seconds. Over months and years, those seconds become hours.

Final Thoughts

By the time, you're done reading this article, one might assume that working across multiple tech stacks .NET, Laravel, and Python could feel chaotic.

For me, I feel like I've found the right ecosystem that just doesn't get in my way and allows me to be consistent, productive and focused on building stuff and problem-solving instead of fighting my tools.

Consistency is one of the most underrated productivity multipliers in software development.

Hope you liked reading this article!

Why Hangfire recurring jobs should always have stable IDs?

Recently, I learned that not giving explicit IDs to Hangfire recurring jobs can silently create duplicates.

When you call RecurringJob.AddOrUpdate(...) without a pre-defined ID, Hangfire auto-generates one based on the method expression. That sounds fine—until you deploy across multiple nodes or refactor your code.

In such cases, Hangfire may generate a new derived ID, while the old recurring job continues running in the background. The result? Duplicate jobs executing on schedule, often goes unnoticed.

Simple fix

Make sure that you always pass a clear, human-readable ID:

RecurringJob.AddOrUpdate<ICustomBackgroundServiceManager>(
    "payments:reconcile-3h", // Human-readable ID
    s => s.ResendPaymentReportToVendorHourly(),
    Cron.Hourly(3)
);

Next, ensure that overlaps are impossible in a multi-node setup by adding this attribute to your job method:

[DisableConcurrentExecution(timeoutInSeconds: 3600)]
public Task ResendPaymentReportToVendorHourly() { /* ... */ }

This uses a distributed lock backed by Hangfire storage—so even with multiple nodes, only one execution runs at a time. If you’re wondering whether this locks the job for the full hour: it doesn’t. The timeout exists for crash-safety, not throttling.

After doing this, recurring jobs become much easier to reason about, identify, pause, or delete in a multi-node environment.

Hope you found this tip useful!

Running Laravel queues with Supervisor on Ubuntu

Laravel queues aren’t new to me, but I realized I’d never really written down how I usually set them up on a fresh Ubuntu server.

Whenever I need queue workers to run continuously — and survive restarts or crashes — I almost always reach for Supervisor. It’s simple, boring, and gets the job done.

First, install it on your server:

sudo apt update
sudo apt install supervisor

Then create a small config file for the queue worker:

sudo vim /etc/supervisor/conf.d/laravel-worker.conf

This is the setup I generally start with:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/project/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/project/storage/logs/laravel-worker.log
stopwaitsecs=600

A few quick notes on what this does:

  • It runs queue:work instead of queue:listen, so the worker stays in memory
  • Two worker processes are started in parallel (numprocs=2)
  • If the queue is empty, the worker sleeps for a few seconds instead of spinning
  • Failed jobs are retried a limited number of times
  • The worker is force-restarted every hour to avoid memory leaks
  • If a worker crashes, Supervisor brings it back up automatically

Once the file is saved, reload Supervisor’s config:

sudo supervisorctl reread
sudo supervisorctl update

Start the workers:

sudo supervisorctl start laravel-worker:*

And check their status:

sudo supervisorctl status

Whenever I deploy new code or change environment variables, I just restart them:

sudo supervisorctl restart laravel-worker:*

Logs end up in Laravel’s storage/logs, which is usually the first place I look when something feels off.

This setup has been reliable for me across multiple projects. No dashboards, no extra moving parts — just queue workers quietly doing their job in the background.

Hope you found this useful!

DateTimeKind.Unspecified can quietly break your dates

Two months ago, a client raised a critical ticket where some users complained that their Start Date of the Financial Year had gone one day backward. This wasn’t caught during UAT while integrating their API, and I was surprised to run into this date conversion bug while I was on vacation (yes, that sucks!).

I had a perfectly normal-looking date:

2025-01-01 00:00:00.0000000

Nothing fancy. But after converting it to UTC, I noticed something odd — the date changed.

Luckily, I was able to trace where it was coming from, and after debugging in Visual Studio 2022, it turned out the culprit was DateTimeKind.Unspecified.

When a DateTime is parsed without timezone information, .NET marks it as Unspecified. If you then call ToUniversalTime(), .NET assumes the value is local time and converts it to UTC. On a UTC+5:30 system, that means:

2025-01-01 00:00 → 2024-12-31T18:30:00Z

Same instant, different calendar date. Easy to miss, painful to debug.

The fix

If your date is already meant to be UTC, you need to say so explicitly:

DateTime.SpecifyKind(inputDateTime, DateTimeKind.Utc);

Or handle it defensively in one place:

switch (inputDateTime.Kind)
{
    case DateTimeKind.Utc:
        return inputDateTime;

    case DateTimeKind.Local:
        return inputDateTime.ToUniversalTime();

    case DateTimeKind.Unspecified:
    default:
        return DateTime.SpecifyKind(inputDateTime, DateTimeKind.Utc);
}

Takeaway

Never leave DateTimeKind to chance especially if you’re working with APIs, audits, or anything date-sensitive.

It’s one of those small details that only shows up when things go wrong — which makes it worth handling upfront.

Hope you found this tip useful!