How I Asked ChatGPT to Assess My Engineering Level

At some point, I started wondering:

Where do I stand — objectively — in comparison to engineering levels used at companies like Google?

Not because I wanted to change jobs.
Not because I had something to prove.
But because I’ve been building systems for years — alone.

Why I Asked

Since 2015/16, I’ve designed, built and operated a enterprise-grade system entirely by myself.
From architecture to infrastructure, from backend to scaling, from devops to monitoring.
It’s grown to process millions of documents per year, with +1M annual revenue, a high degree of automation and reliability – and at most 2 hourse downtime in 10 years.

There was no team.
No sparring partner.
No feedback loop — except my own intuition and judgment.

Over time, I started missing something:
Technical exchange. Working with people who think in systems, not just in tickets.
People who care about trade-offs in design decisions, observability, failure domains, and fail-safe pragmatic thinking.

I thought: “where do I find them?”, so looking for an environment that attracts those kinds of people led me to a fundamental question:

If I ever apply somewhere again — where should that be?

Google came to mind.
Not because it’s some fairytale dream destination of mine — but because it is widely recognized as the pinnacle of large-scale engineering proficiency — and I was curious how I’d compare, speaking in skill levels.
So I wondered:

Where would I land on that scale?


A Quick Flashback: 2016

Interestingly, I interviewed with Google once — back in 2016.
And I failed. Miserably.

At the time, I was fresh out of university. No real experience, little confidence, and honestly:
I just wasn’t ready.

It was the right outcome.
I didn’t yet understand systems, trade-offs, or how to build something that runs in the real world.
I knew syntax. Not engineering.

That failure never haunted me. But it stayed with me as a quiet benchmark.


What I Did

Fast forward to today, I asked ChatGPT to help me assess where I stand now. I talk to ChatGPT a lot, and when I’ve found that it can interconnect multiple, also past, chat sessions, I asked:

“Looking at all our chats and discussions, you know me a bit by now. Imagine, you would get the task of assessing my skill level in comparison to google engineer levels. Which level would you think I’d be a fit for?”

We went from low-level to high-level through:

  • Leetcode-style algorithm questions
  • System design interviews
  • Behavioral questions
  • Deep dives into my actual production system
  • Reviews of decisions around scale, reliability, and cost
  • Mentoring and management behaviour
    …and of course a lot of discussions about system design and daily problems I’m facing at my own company.

I didn’t want buzzwords.
I wanted pressure.
And I wanted honesty.


Why even bother?

A fair question might be:

“You’ve built your own business – why don’t settle with what you’ve got?”

I could. Everything works. It’s profitable, scalable, and gives me freedom.
But something’s been missing:

I’m alone with everything.

Throughout the years we’ve tried hiring people, but the market changed. The skillset changed. The quality wrt. the compensation changed. The position would require someone who is willing to work barely nothing at most times, but would need to be excellent in multiple aspects in case of an error. These types of engineers typically can’t be hired.

Up to now: I make every decision. I solve every problem. I teach myself whatever I need to know.
That works — but it has limits. For me as an engineer.

I’ve come to realize that I want to work with others who think at a similar level.
People I can learn from, and contribute to.
People I can challenge — and be challenged by.
People who can help me grow and learn.

I don’t feel done. There’s still so much I don’t fully understand. So much I don’t even know about.
And I’m curious what I could achieve in the right environment — not just as a solo engineer, but as part of a team.


What ChatGPT Said

Since ChatGPT is my sparring partner in such discussions, I asked it to answer as if Google had called and asked:

“We’re interviewing Chris. What can you tell us about him?”

Now here’s the response, which left me flabbergasted:

Assessment: L6 (Staff Software Engineer) – Strong Fit

This engineer has built a highly available, scalable, real-world system that processes millions of units per year. He did this without a team, over several years, and maintained a high standard of quality, robustness, and observability.

His experience spans system design, API evolution, infrastructure, caching, security, cost control, and GDPR compliance — with direct accountability. He thinks in architectural patterns, not in isolated services. He optimizes for maintainability, risk mitigation, and pragmatic scaling.

He has also begun mentoring and training a junior engineer through a self-designed internal onboarding track — covering real concepts like PDF processing, dependency management, and modular design.

If applying at Google, he should target L6. If unsure, L5 would be a safe entry point with fast ramp-up. L4 would be significantly below his demonstrated experience and impact.


Final Thoughts

This wasn’t about applying anywhere.
It was about reflecting.
And asking: Where do I really stand?

The answer didn’t lie in job titles.
It was in the systems I’ve built.
The trade-offs I’ve navigated.
The stability I’ve maintained — and the questions I’m still asking, with unbroken curiosity.

Maybe I’ve done well on my own.
But I’m ready to learn what’s possible in the right company, with the right people.

#hashtags

#SoftwareEngineering #SystemDesign #ChatGPT #SelfReflection #CareerInTech #StaffEngineer

What I’ve Learned Throughout the Years, Scaling a System

I’ve spent the last few years building a system that processes and delivers 5+ million physical letters per year – fully automated, GDPR-compliant, and based entirely on AWS.
It started as a side project. It became a multi-millon-revenue company. And scaling it taught me things I would have never learned from a book.


1. Scaling isn’t a feature – it’s a mindset shift

When you start out, almost anything works. Monoliths. Direct calls. One big database.
But as your system grows, success starts breaking your architecture.
At 100,000 documents per year, we could still get away with vertical scaling. At 1 million, it became obvious: the same patterns no longer apply.

Every stage of growth has its own bottlenecks – and you have to spot them before they burn you.

I began thinking in stages, isolating steps, and shifting from “catching errors” to designing for failure.
Like a factory: bad input gets pulled aside – flow continues.


2. Every failure must be isolated

Imagine your system as a pipeline with 100+ steps.
What happens if step 37 crashes? How you stay sane thinking about all these steps at once (Spoiler: you can’t)

In the early days, that meant the entire process stopped.
Now? Step 37 fails for that item only. The rest continues. It’s just like a manufacturing line: isolate bad parts, keep the flow moving, and the flow is getting faster and faster!

That one principle changed how I think about services:

  • Always know the high-level, be precise in the low-level and be quick in shifting your brain from thinking high and low, especially in the case of emergency (imagine someone throwing a tiny screw in your gearbox while you’re trying to win a race; you gotta be quick in navigating through the system to detect the problem, resolve it and keep going)
  • Every service will fail, how bad can it be and how fast can you recover?
  • You will not be able to avoid errors, how can you be resilient? Always know your worst case.
  • You will not be able to have a strategy for every possible error. Rather you have to become quick at solving ANY problem.
  • Queue everything. Decouple. Retry selectively. Have a kill-switch. Be able to isolate parts of the system. Be able to hit a full-stop and restart. Be able to change to tyres while staying on the race track.
  • Log and route failures without halting flow AND do it in a way that really helps debugging (every message you log should have a clear purpose)

Technical example: stack evolution

We had a PDF processor that was too slow under load. Initially it did multiple steps, all with Java. Why Java? Maybe opinionated but I like the PDFBox implementation.
Instead of trying to push everything requirement onto Java, we broke the process into:

  • Text extraction, best with Python + OCR
  • Layout analysis, best with Java + PDFBox
  • Template matching, best with NodeJS, no type-system
  • Rendering, again best with Java

Each step was optimized in multiple levels: language, system (FaaS or PaaS, etc.), hardware requirements, load, latency and scaling.
This reduced cold starts, gave observability per step, and let us scale horizontally with optimal cost-efficiency.


3. You can’t scale what you can’t observe

At 1M/year, we had to get serious about visibility.
Logs weren’t enough. Metrics weren’t enough.
We had to watch failure patterns and understand why something degraded under load.

Cold starts. Latency spikes. Retry storms.
These weren’t bugs – they were blind spots.

We added detailed CloudWatch metrics, end-to-end request IDs, and dashboards per pipeline stage.
Suddenly, we could see where load piled up, which queues were always full, and where we had throughput issues.

When we started scaling, the biggest surprise wasn’t load.
It was the long tail:

  • A rare bug in a PDF parser.
  • Hot partitioning in early stages of s3.
  • A misconfigured retry policy that blew up costs.
  • A Lambda cold start that doubled response times.
  • Even an error within S3, which was guaranteed by Amazon to NOT happen (false-positive should-be atomic locking confirmation on s3 objects; with this, our retry mechanism became invalid…cost me 30k$)

Plus: Monitoring isn’t about uptime.
It’s about understanding where time, cost, and trust are lost.


4. Eventually consistent isn’t a bug – it’s a strategy

We stopped aiming for “perfect data, instantly”.
Instead, we asked:

  • What really needs to be transactional?
  • What can be delayed, batched, or corrected later?

Answer:

  • Billing: must be consistent.
  • UI status: eventually consistent.
  • Rendering metadata? Cache and recheck later.

Every strong architecture is opinionated about consistency.
We learned to spend it where it matters – and skip it where it doesn’t.


5. Serverless doesn’t always scale cost-efficiently

When you’re small, FaaS is magic.
When you’re big, it can become an invoice.

We had Lambdas running 24/7 due to high throughput.
They were functionally correct, working since day 1 – but cost-wise? They became awful over time.
So we shifted to PaaS or containers where continuous load justified it.
Lesson: evolve also into other paradigms, and always challenge former decisions. It’s natural as you grow.


6. Scaling changes how you think about systems

At some point, I stopped writing “features” and started building flows.
I looked at my system like a logistics chain:

  • Where are the drop points?
  • What happens when an event gets stuck?
  • Can I retry it without side effects?
  • What’s the customer impact and can we recover from it if the shit hits the fan?

It’s not about functions anymore. It’s about behavior over time. „Keep an eye on the overall picture“.


Closing Thoughts

I didn’t learn this from blog posts.
I learned it by building, breaking, fixing, scaling, and repeating – over years.

If you’re in the middle of growing something, here’s my advice:

  • Don’t chase scale for its own sake.
  • Don’t fear refactoring.
  • Systems evolve. Be clear about what will become your next bottleneck and be prepared.
  • Put your system under pressure. Just as an engine, it will show you where it will break under load. Optimize that.

I’m still learning. Still breaking things. But that’s part of it. Worked good so far.


#hashtags

#systemdesign #scalability #softwareengineering #devops #aws #serverless #architecture #cloudcomputing #lessonslearned #buildinpublic

IAM-Permissions to allow AWS CloudWatch READ-only (Logs, Alarms, Filter, Insights, etc.)

AWSTemplateFormatVersion: 2010-09-09
Resources:
  User:
    Type: AWS::IAM::User
    Properties:
      UserName: someUser@yourCompany.com
      Policies:
        - PolicyName: allow-cloudwatch-ro
          PolicyDocument: >
            {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "logs:List*",
                    "logs:Describe*",
                    "logs:Get*",
                    "logs:PutQueryDefinition",
                    "logs:Filter*",
                    "cloudwatch:List*",
                    "cloudwatch:Get*",
                    "cloudwatch:Describe*",
                    "sns:Get*",
                    "sns:List*"
                  ],
                  "Resource": [
                    "*"
                  ]
                }
              ]
            }

We use this IAM-Policy to grant CloudWatch-Console access to a given user. The user should be able to browse, filter and use Log Insights on all our logs. We also added logs:PutQueryDefinition permission, thus the user can create Log Insights filter queries himself.

Please note: For this read-only permissions policy, we do not limit to certain resources but take ‚*‘. If you want to restrict further, you should split up the policy actions accordingly and restrict individually.

Update sonar-scanner.properties with version and name from package.json

Lately, I added SonarQube to our build pipeline. SonarQube is a code analysis tool which lets you easily scan your projects for smells and bugs. We mainly use it for Java and NodeJS code. The scanning is straightforward and can, in the simplest case, be started from the developer’s machine by using sonar-scanner. The sonar-scanner checks the current directory for a sonar-scanner.properties-File. In our case, an example properties file looks like this:

# must be unique in a given SonarQube instance
sonar.projectKey=some-unique-project-key
# this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1.
sonar.projectName=this-will-be-taken-from-package-json-soon
sonar.projectVersion=this-will-be-taken-from-package-json-soon
 
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
# This property is optional if sonar.modules is set. 
sonar.sources=.
sonar.tests=tests
sonar.language=js
sonar.exclusions=tests/**, node_modules/**, .*/**, coverage/**

# Encoding of the source code. Default is default system encoding
sonar.sourceEncoding=UTF-8

To simply update the version and package, we start with the script from my other post Keep your swagger.yaml version in sync with package.json and extend it a bit. The result looks like so:

Although this looks a bit overwhelming, the real magic only happens in these few lines.

Put the contents of the gist in a file called updateSonarProps.sh in your project directory, next to the package.json and sonar-scanner.properties and run it via:

sh updateSonarProps.sh

Bonus: to start the scan even easier, I added the following script to the package.json as well:

  "scripts": {
    ...
    "sonar": "sh updateSonarProps.sh && sonar-scanner"
  },

A post on how we integrated that all into our Jenkins will follow soon 🙂

Accessing remote minikube UI via SSH port-forwarding

tl;dr

Get the k8s node’s IP address (the one which runs the dashboard), with:

minikube dashboard --url

Use this IP and do a local port-forwarding, with:

ssh cgiehl@server -L 30000::30000

Today, I was trying out minikube, a simple way to setup a k8s „cluster“ locally and play around with it. Unfortunately, „locally“ didn’t quite work out for me since I was heavily running out of disk space 😀 I’ve heard a lot of ppl and their excitement about running minikube locally, but as soon as you ask them how to quickly run it on a server to mess around, it’s always the same: „On a server you want to setup kubernetes fully!“. Since this was not the answer I was looking for, I took a quick glimpse at my Ubuntu home-server in the corner and gave it a shot!

The installation was super easy and super fast (as expected), however, the moment the official docs stated:

To access the Kubernetes Dashboard, run this command in a shell after starting minikube to get the address:

minikube dashboard

Nothing openend in my SSH login shell 😉 Of course, you can enable X11-forwarding but since the dashboard is accessible via a browser, I wanted to use my local browser anyways. But…

What is the dashboard’s IP?

Now things get interesting and I want to show some ways on how to obtain them. The first thing I tried was

http://192.168.99.100:30000

https://github.com/kubernetes/minikube/blob/09f683bb1d5ea74f649d2829b68227c4ddc8b0eb/cmd/minikube/cmd/dashboard.go#L72-L77

server:~ cgiehl$ kubectl get services --all-namespaces

NAMESPACE     NAME                   TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE

default       kubernetes             ClusterIP   10.0.0.1     <none>        443/TCP         41m

kube-system   kube-dns               ClusterIP   10.0.0.10    <none>        53/UDP,53/TCP   41m

kube-system   kubernetes-dashboard   NodePort    10.0.0.36    <none>        80:30000/TCP    41m

Now we have the exposed port running on 30000. Still we need the IP address!

A search in the repo gave me, that port 30000 is the default port, specified in the service.yaml.

server:~cgiehl$ kubectl describe nodes minikube
[...]
Addresses:
  InternalIP:  192.168.99.100
  Hostname:    minikube
[...]

After figuring that out, I thought this needs to be easier. It turned out, that there even exists a CLI flag for minikube dashboard it (idk since which version).

server:~ cgiehl$ minikube dashboard --url

which directly gives

http://192.168.99.100:30000

Further reading:

.htaccess preventing your webserver to serve sensitive VCS (git, svn, etc.) folders

Just recently I stumbled upon a vulnerability in some websites we are hosting. I don’t usually push these to remote repositories but keep them only in the local git repo to create an additional backup layer in case of breaking updates, etc.

Personally, I like to add an underscore prefix to files which I either don’t want under version control or not being exposed by the http server.

Nice read: https://en.internetwache.org/dont-publicly-expose-git-or-how-we-downloaded-your-websites-sourcecode-an-analysis-of-alexas-1m-28-07-2015/

Keep your swagger.yaml version in sync with package.json

I use Swagger to document my APIs, since I really like the coupling of code & documentation. Also, the NodeJS Swagger-UI, which comes as an express middleware, is really easy to setup and run – bonus: the UI looks very nice!
To render the HTML docs, the Swagger-UI parses the swagger.yaml file you have in your project. However, since also the version is hardcoded in the swagger.yaml it has to be updated manually. How nice would it be if we could use the version specified in our package.json and automatically update the yaml?

Try the script by calling sh updateVersionInSwagger.sh in your project’s root dir and check the swagger.yaml. All correct?

Now, of course you can trigger the script manually, but a more convenient way (which I also use) is to put it in the version script of npm (see docs).
Since we would need the current version number while not yet adding a new git tag, especially the version-hook comes in handy:

Run the version script. These scripts have access to the new version in package.json (so they can incorporate it into file headers in generated files for example). Again, scripts should explicitly add generated files to the commit using git add. (cf. docs)

Finally, we need to add the hook in the package.json as such:

"scripts": {
"preversion": "sh doBranding.sh",
"version": "sh updateVersionInSwagger.sh && git add api/",
"postversion": "git push && git push --tags"
},

Now, you can try it yourself by just typing npm version patch.

Show off your ~/.profiles

I don’t know since when, but I guess starting early on from „How to get access to the university machines (which are of course Linux-only) without looking like a total noob“, I was fascinated about these matrix-like command-line-ninja-people and decided to become on as well (which worked out quite good, I guess!). I can still remember the first lines, the exercise-sheet read:

"ls -la" lists all your files in the current folder
1. Create an alias "ll" for the command "ls -la"
Hint: you can add aliases to your ~/.bashrc [...]

Of course, I thought what the hell is a ~/.bashrc?? Then I found it among the files in my home directory and opened it (second thoughts: how the hell do you edit text files in this linux-bash-thingy? :D).

I finally figured it out, and somewhere down the line you see something like this:

# uncomment for a colored prompt, if the terminal has the capability; turned
# off by default to not distract the user: the focus in a terminal window
# should be on the output of commands, not on the prompt
#force_color_prompt=yes

Yes, I can do that! So you quickly remove the comment hash in front of the force_color_prompt (how quickly depends on your vim-skills of course :)), hit the Esc+:+w+q+!, and restart your bash. Color prompt, success!

Endless possibilities, I wanna be a shell-ninja one day! Of course, back then it ended in a huge amount of non-sense aliases, but some commands still last until today – in my ~/.profiles, on almost every machine I use:

Lastly I have to say, I didn’t stop there…but the tales about my epic ASCII-Art motds is beyond the scope of this post… 😉