Don’t Start Your Software Career At FAANG!

My career so far looks like this: startup(s) ->  Amazon -> startup(s).

And if you are a new graduate or just starting to work, I recommend a similar path. Don’t try to jump to FAANG right away even if the money looks life-changing or if you think it will help your resume. In this post I’ll attempt to explain why. (I also want to apologize in advance if this post sounds like I hate Amazon. I don’t. It’s just… my experience could have been better).

The learning curve

I remember when I first started at Amazon in 2017, I got bombarded with new terms: Brazil, LPT, pipelines, CRUX, Apollo… The list was endless. FAANGs tend to build everything in house, so you will have a very painful time learning every tool, because usually their documentation is terrible. After all, why bother writing extensive documentation, if the population consuming it will 100% forget they exist after they quit?

But, once I figured out the “real world” equivalent to each tool, it became easier to onboard: Brazil was Maven, pipelines was TeamCity, LPT was Pulumi, CRUX was Github’s PR, Apollo was GitHub actions. If you know how each “real world” tool works, you will have an easier time mapping things to know to things you don’t.

Conversely, if you start at FAANG and then move elsewhere, you won’t be able to ask your coworkers “is this tool like Brazil?”, because, unless they have worked at Amazon, they will look confused and tell you that Brazil is a country.

The bad practices

The other downside to starting at FAANG is that you may learn practices that are frowned upon in smaller companies.

One of those practices is the well-known “promotion oriented architecture” that means that people will over-complicate and and over-provision their projects just so that they can earn a promotion to the next level. This will hinder you at startups, where trying to design a system that can handle 10,000,000 RPS when you don’t even have the customers to reach 10 RPS will only get you angry stares. Designing for “what-ifs” is okay at big companies, but less so in startups, where delivering something quickly is of much higher value (and cheaper) than delivering something that can scale.

Even after forgetting about money, designing a big project means that *someone* will have to feed the monster afterwards. And guess what, people at big cos leave quicker than you can sneeze. Soon, nobody will know the how’s or the why’s of that big project, and you, the new hire, will be the one tasked to pick up the pieces. At startups, if things go well, people tend to stick around for longer, which means there will always be someone that knows the why or how of things. (If nobody does, the project probably costs money for no reason and will be axed anyway).

The other practice that is not encouraged at big companies (or at least, I didn’t see it) is being proactive, or trying to take on more tasks that correspond to you. It’s very normal in big companies to ONLY work on the tasks that you are assigned, and not even think about what other teams are doing and how your work pieces with theirs (one time, I witnessed two teams working on the same project without knowledge of each other). When something breaks on some other team, instead of looking into it you are encouraged to offload it to them. I’ve seen this happen countless times and it has a name: “ping-pong tickets”. Also called “not my job”.

At FAANG, unless you transfer teams every now and then, it’s very easy to get “stuck” in the work within a team. You probably won’t get a broader vision of how the bigger puzzle works, because you aren’t encouraged to look beyond your team, and you aren’t encouraged to look for collaboration opportunities or ownership opportunities.

In startups, it’s encouraged to explore different ideas and to take ownership of things. If I see an issue in the front-end, I can go ahead and try to solve it. If I see a poorly written or incorrect piece of documentation, I can send a fix for it. If I see a CFP, I can submit a proposal for a talk! The possibilities are endless. Which means, the growth is unbounded. And because money and time are tight, you have more constraints, which fosters creativity.

The slowness

Other people’s experience at FAANG might have been better than mine, but in my case, my onboarding felt very uncomfortable. I started working on the amazon website and I had so many questions about everything but didn’t know how to ask without feeling like I was bombarding my peers. And I didn’t see everyone as puzzled as me, so my imposter syndrome ballooned. I didn’t feel fully productive until at least a year in.

In a startup, everyone is busy all the time, but because we’re a small group, we’re closer to each other so it’s easier to feel more at ease when asking questions (and if I don’t want to bother people, I read the plentiful open source docs). At FAANG, I got the sensation that everyone put themselves and their tasks first, and didn’t care that much if their peers were struggling. My code reviews never took less than a day. Everything (and I mean, everything, even getting a response in a ticket) takes at least a day. Work was very slow, and eventually, I got used to the slowness.

The slowness got so bad that I had a metric for myself called “how many times per day do I stare at the clock waiting for stuff to happen?”. At startups, that metric tends to zero. At Amazon, on some days it got so bad that i’m certain it paged an on-call somewhere.

Conclusion

Working at FAANGs has its upsides, of course. You get to see complex things built. There’s a wealth of knowledge to be found, if you know how to look for it. And the bar for operations is very, very high, and hopefully you will carry that bar to every job after it.

But in my experience they can hinder your career more than it can help it because you can fall into the “not my job” mentality. In my jobs after FAANG, nobody was very impressed that I worked there, and I can’t say that I was hired because I worked for FAANG.

Don’t Start Your Software Career At FAANG!

From Kindle to Kobo

kindle to kobo
Making the move 😉

Problem: I had a Kindle Paperwhite and I bought a Kobo Libra Colour. I also had bought books for my Kindle. How do I transfer them to Kobo?

Solution: on Windows 10.

  1. Uninstall Kindle for PC (if you have it).
  2. Install Kindle for PC 1.17 and
    1. Disable automatic updates
    2. Login to your Amazon account
    3. Select a location for your books
    4. Download all the books that you want to transfer – they should be in AZW format.
  3. Download NoDRM
  4. Install Calibre and
    • Install the NoDRM plugin via a path to the ZIP that you just downloaded
    • Within the NoDRM plugin settings, add the serial number to your Kindle. (Find it in https://www.amazon.ca/hz/mycd/digital-console/alldevices)
    • Add your books from the folder from step 2.3
    • Select all books, right-click, “convert books” and “bulk convert”
    • Select KEPUB as the target format.
    • Click “OK”.
From Kindle to Kobo

Tips & Tricks for passing the driving test in BC, Canada

The N test lasts ~30 minutes and it’s easy, but there’s many things to remember.

Before:

  • Practice 3-point turns.

The day of:

  • Remove dashcams
  • Remove bike racks covering the license plate
  • Remove anything heavy from the backseat

The test:

  • Watch the speed limit. You’re allowed to go slower than usual, but not faster.
  • Watch for parks & playgrounds. Drive around the area where your test will be to know where they are. Also watch out for speed bumps, they are usually an indicator that you are near a park.
  • Shoulder check when changing lanes, changing direction, or parking.
  • 360 check before reversing.
  • Handling boulevards:
    • If you are turning left and the boulevard is parallel to you:
      • if the cross street has traffic light: wait as you normally would
      • if the cross street does not have traffic light: advance the car, and wait next to the far end of the boulevard.
    • If you are turning left and the boulevard is perpendicular to you:
      • Advance your car straight until you are in the middle of the boulevard. This reduces the time it will take you to make the turn.
  • Stay in the middle of your lane, and on your lane.
  • Wait for a safe gap when turning or when at a stop sign. Don’t rush.
  • When leaving a back alley, stop before turning. Also the max speed is 20 km/h.
Tips & Tricks for passing the driving test in BC, Canada

Recetas

Recetas

“Kubernetes: Up and Running” Book Notes

Kubernetes provides abstractions to build decoupled services:

  • Pod: group of containers running in the same environment. Ask yourself: “will the containers work correctly if they land on different machines?”. If the answer is NO, a pod is the correct grouping for the containers. E.g. WordPress and MySQL shouldn’t be grouped into a pod.
  • Services: provide load balancing, naming, service discovery
  • Namespaces: provide access control and isolation. Think of each namespace as a folder holding a set of objects.
  • Ingress objects: provide a frontend for multiple microservices
  • Cluster: a collection of nodes, each of which can have multiple pods, that is meant to live in a single region.

Containers are usually launched by a daemon on each node called a kubelet.

Minikube provides a way to get a local Kubernetes cluster up and running, in your laptop or in a VM.

The Kubernetes proxy is responsible for routing network traffic to load-balanced services in the Kubernetes cluster. It must run in all nodes in the cluster.

Kubernetes also runs a DNS server, as a replicated service on the cluster.

A Kubernetes context is like a shortcut that let you quickly switch between different cluster configurations. The context details are stored $HOME/.kube/config.

Objects in Kubernetes API are represented as YAML files. If an object is stored in obj.yaml, you can create it by doing: kubectl apply -f obj.yaml.

kubectl port-forward <pod-name> 8080:80 opens a connection that forwards traffic from the local machine on port 8080 to the remote container on port 80.

Kubernetes uses declarative configuration for each object (like Terraform).

A pod can have:

  • liveness check: determines if an application is running properly. If this check fails, the container is restarted.
  • readiness check: determines if an application is ready to serve requests. If this check fails, the container is removed from service load balancers.

Labels are used to identify and select objects in a cluster.

The Service object operates at the TCP and UDP layer of OSI (layer 4).

The Ingress object operates at the HTTP layer (layer 7). It is Kubernetes way of implementing the “virtual hosting” pattern wherein many HTTP sites are hosted on a single IP address by having the component route, based on the Host, Port and URL of the request, the server that will receive the request.

A ReplicaSet ensures that the right types and numbers of pods at running at all times. Most people will prefer creating a ReplicaSet instead of Pods.

A Deployment manages ReplicaSets. It also helps roll out a new version of the software running in one or more containers. There are several deployment strategies:

  • Recreate: fast, but it will cause downtime.
  • RolingUpdate. Takes two parameters. maxUnavailable and maxSurge, controlling how many extra resources can be created to achieve a rollout. A Blue/Green deployment can be achieved by setting maxSurge=100%.

A Deployment can be configured with minReadySeconds and progressDeadlineSeconds. The former indicates time to wait for a Pod to become healthy. The latter controls how much to wait for the entire Deployment to make progress.

A DaemonSet is used to ensure that one copy of a Nod is running across a set of nodes in a cluster. For example, a log collector or a monitoring agent.

A Job creates Pods that run until successful termination. This is useful for database migrations or batch jobs. It can be configured with completions and parallelism.

A ConfigMap is an object that defines a small filesystem. It can also be thought as a set of variables to define an environment.

Secret data can be exposed to Pods using the Secrets volume type.

Kubernetes uses roles and role bindings for authorization. A role is a set of capabilities. A role binding is an assignment of a role to one or more identities. A Role and RoleBinding are scoped to a namespace, whereas a CluserRole and ClusterRoleBinding are scoped to a cluster. An aggregation rule can combine multiple roles into a new role. It’s also possible to put several identities in a group.

One can test authorization with the kubectl auth can-i <action> <object> command.

The service mesh APIs (Istio, Linkerd) can add complexity, but they provide: network encryption, authorization, traffic shaping, and observability. Encryption is achieved via MTLS. Traffic shaping is routing requests to different service implementations based on the characteristics of the request (.e.g to run A/B experiments).

The Kubernetes API is extensible. E.g. via the operator pattern. You can also create custom resource types (e.g. a LoadTest type) or admission controllers. It’s also possible to use the Watch API to get notified when objects change and react to that.

Pods should have SecurityContext declared to limit what it can do (e.g. what kernel calls it can make, what files it can access, etc).

One should also define a NetworkPolicy to declare how Pods can communicate with each other.

To deploy a Kubernetes cluster it is useful to use parameterized environments, which use template files and parameters to produce a final configuration. One such tool is Helm.

“Kubernetes: Up and Running” Book Notes

LeetCode patterns in Go

PatternDescriptionExample usagesWorks on
Two pointersIterate over an array or multiple arrays to achieve some goal, faster than O(n^2)Finding palindromes, merging two arrays, subsequences in stringsUnsorted arrays, sorted arrays, strings
Sliding windowFind a subarray that satisfies some numerical constraint. (If the constraint involves counting, use a hashmap).
Add elements from the right until the constraint is broken, then remove elements from the left to make the window valid again.
Length of window = right - left + 1
Note: doesn’t work well with binary arrays.
Note 2: the number of valid windows ending at index right is equal to the length of the window.
Length of longest subarray whose sum is equal to some target numberUnsorted arrays, sorted arrays, strings
Prefix sumBuild an array p where p[i] is the sum of all elements up to index i (inclusive). Don’t forget p[0]=0.
Prefix sums allow us to find the sum of any subarray in O(1)
Running sum of arraysArrays of numbers
HashingCount elements, checking for existenceStoring frequency of prefix sumsArrays, strings
Fast & slow pointersLike two pointers, but for linked lists. The slow pointer visits every node, the fast one visits every other node.Find the middle element of a singly linked list (when the fast pointer reaches the end, the slow pointer will be in the middle)Linked list
Dummy nodeCreate a new node if you think you will have to edit the head node.

dummy := &ListNode{Val: 0, Next: head}
curr := dummy
for curr.Next != nil {
// ...
}

return dummy.Next
Linked list
StackLIFO data structureImplement pre-order DFS
QueueFIFO data structureImplement BFS
DequeLike queue, pronounced “deck”, you can also remove from the end and add to the end?
Monotonic queue or stackSortedFind the next element based on some criteria, for example the next biggest elementLinked list, array
DFS (depth first search) Preorder (left -> root -> right), inorder (root -> left -> right), postorder (left -> right -> root).
Can use simple recursion or a stack, but prefer recursion.
Find height of a tree, find sorted array out of a binary search tree (BST)Trees, graphs
BFS (breadth first search)Must use a queue of nodes and a “seen” map to avoid visiting the same state more than once.
Determine: What are the nodes, what are the edges, where should the BFS start?
If you can visit one node more than once, the state key must be the node plus some metadata, eg which edges you used to arrive to that node.
The queue can also include metadata, e.g. how many steps you needed to get to the node.
Explore levels in a tree, find shortest paths in an unweighted graph, find nodes at K distanceTrees, graphs
Dijkstra’s algorithmGreedily pick the shortest weight to visit nextFind the shortest path in a weighted graphWeighted graph
Min/Max HeapAdds and removes root in O(log N). Finds the root in O(1).
If you want the K closest points, insert all elements in a max heap that has up to size K. If more than that, pop from the heap. At the end, the heap will contain the minimum elements.
Repeatedly find min or max element, find the K min or max elements.Trees, arrays
Binary searchFind if an element exists in O(log N) time.
Usually can be used in a greedy algorithm. If we can discover some kind of monotonicity, for example, if condition(k) is True then condition(k + 1) is True, then we can consider binary search.
Arrays, binary trees
Greedy algorithmsMakes the locally optimal decision at every step.
Usually you must sort the data at the beginning.
Find maximum or minimum of somethingArrays
Dynamic programmingThe key is to remember past states to save time.

Find the recurrence relation, the base cases, and then build the function that will compute the answer to the problem, using top-down (recursion plus a cache) or bottoms-up approach (recursion plus an array or matrix).
Problems that have lots of overlapping subproblems.
Find maximum or minimum of something, find all paths in a graph
Arrays, matrix
BacktrackingAn optimization to brute force search. We prune paths that cannot lead to a solution, generating far fewer possibilities.
To avoid duplicates in a backtracking call, pass an “index” variable to the backtracking function to tell it at what point in the input array to start from.
Finding all permutations and combinations, find all unique solutions to something, or find if there is a solutionArrays, strings, matrixes with length <= 15
Difference arrayBuild an array with the changes.

Then use prefix sum to see whether we go over some constraint.
Events happening on a line (e.g. trips on road, water hoses on a garden)Arrays
TrieEfficiently store and retrieve stringsAutocomplete, spellcheckerArrays of strings
Bit operationsGrab the right-most bit: x AND 1
Flip the right-most bit: (x AND 1) XOR 1
Create a mask where the 3th bit is set to 1: 1 << 3
Use an int to represent a set and then efficiently check for membershipNumbers, arays
Line sweepAnalyze data on an X-Y plane. For each event, record its start and end points along the X-axis. When the sweep line encounters a start point, add 1 to the count. When it encounters an end point, subtract 1. By sweeping across the X-axis, you can track changes in the number of active events at any given pointFind population count in a year, find interval overlapEvents with a start and end time

Max & Min Heaps:

h := &IntHeap{}
heap.Push(h, k)
heap.Pop(h)

type MaxHeap struct {
	IntHeap
}

func (h MaxHeap) Less(i, j int) bool { return h.IntHeap[i] > h.IntHeap[j] }

// min heap
type IntHeap []int

func (h IntHeap) Len() int           { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) {
	*h = append(*h, x.(int))
}

func (h *IntHeap) Pop() any {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}

Sorting functions:

sort.Slice(people, func(i, j int) bool {
        // ascending order
	return people[i].Age < people[j].Age
})

smallInts := []int8{0, 42, -10, 8}
slices.Sort(smallInts)

sort.Strings(s)

Queue and Stack:

queue := list.New()
queue.PushBack(...)
queue.Remove(queue.Front())

stack := list.New()
stack.PushBack(...)
stack.Remove(stack.Back())

Regexes:

re := regexp.MustCompile(`(?P<age>\d{2})`) // eg 42
matches := re.FindStringSubmatch(personDetails)
age := matches[re.SubexpIndex("age")]

Circular linked list:

r := ring.New(8)
for i := 2; i <= 9; i++ {
	r.Value = Phone{
		Number:  i,
		Mapping: []rune{},
	}
	r = r.Next()
}

Bitwise operations:

  • &: Bitwise AND
  • |: Bitwise OR
  • ^: Bitwise XOR
  • <<: Left shift
  • >>: Right shift

Greatest common divisor:

// euclidean algorithm
func gcd(a, b int) int {
	if a == 0 {
		return b
	}
	return gcd(b%a, a)
}
LeetCode patterns in Go

Handling program interruptions gracefully in Go

package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
        "syscall"
	"time"
)

func main() {
	fmt.Println("Press Ctrl+C to exit")
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill, syscall.SIGTERM)
	defer stop()

	waitForExit := make(chan struct{})
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("before exit")
				time.Sleep(1 * time.Second)
				fmt.Println("after exit")
				waitForExit <- struct{}{}
				return
			default:
				fmt.Println("long running task")
				time.Sleep(1 * time.Second)
			}

		}
	}(ctx)

	<-ctx.Done()
	fmt.Println(ctx.Err())
	<-waitForExit
}

Demo:


[30/07/24 9:01:29] ~/GolandProjects/programInterruptDemo  $ go run ./main.go
Press Ctrl+C to exit
long running task
long running task
long running task
long running task
long running task
^Ccontext canceled
before exit
after exit
[30/07/24 9:01:46] ~/GolandProjects/programInterruptDemo  $
Handling program interruptions gracefully in Go

Errors in Go

From simpler to more complex:

1. errors.New() or fmt.Errorf() without a sentinel error

  • Only use for quick prototyping

2. Sentinel errors

  • When you have an interface and want implementations to be able to throw errors and then catch them to run some logic
  • But! you won’t be able to customize the error thrown
var (
  ErrSyntax = errors.New(“syntax error”)
)

3. fmt.Errorf()

  • When you want to throw a sentinel error and also add customized details
  • But! you won’t be able to programmatically get the customized details
err := fmt.Errorf(“%w: line %d”, ErrSyntax, line)
fmt.Println(err) // “syntax error: line 11”

errors.Unwrap(err) // Returns ErrSyntax.
errors.Is(err, ErrSyntax) // true.

4. Custom error types

  • When we want full control of the error and wrap any other kind of error
type DisplayError struct {
  Message string
  Error error
}
 
func (e DisplayError) Error() string {
  return e.Message
}
 
func (e DisplayError) Is(err error) bool {
  return errors.Is(err, e.Error)
}
 
// Usage example
 
var err error = DisplayError{Message: "Please try again later", Error: configprovider.ErrTimeout}
 
if errors.Is(err, configprovider.ErrTimeout) { // true
  fmt.Println(err) // "Please try again later"
}

Errors in Go

Solving Identity Management in Modern Applications – Book Notes

Events in the life of a user:

  1. Provisioning
  2. Authorization (granting of privileges)
  3. Authentication
  4. Access policy enforcement
  5. Sessions
  6. Single sign-on (a user authenticates once and accesses multiple applications, without having to reauthenticate because they all trust the same identity provider)
  7. Stronger authentication (supported by both OIDC and SAML 2)
  8. Logout (may involve terminating more than one session)
  9. Account management
  10. Deprovisioning

Protocols

  • SAML 2: solves the problem of single sign-on across domains. It’s complex to implement but it’s widely used.
  • Ws-Fed: also solves single sign-on.
  • OAuth 2: allows a user to authorize one application (the “client”) to send a request to an API (the “resource server”) on the user’s behalf, without needing their credentials. It solves authorization. Current version is OAuth 2.0 but 2.1 is on the works.
  • OIDC: layer on top of OAuth 2 that provides information to applications about the identity of authenticated users. Solves authentication.
  • SCIM (System for Cross-Domain Identity Management): solves the problem of sending and updating identity information from one domain to another. It provides a standard REST API for one system to send requests to another for adding or modifying user and group records. See this blog post.

OAuth 2

  • Example: user is writing documents in WriteAPaper.com using his own documents stored in documents.com. So WriteAPaper.com requests access to those documents with the user’s consent.
  • An authorization server must implement the /authorize endpoint and the /oauth/token endpoint, which accepts a code.
  • Three grant types / flows:
    • Authorization code with PKCE (Proof Key for Code Exchange): the code ensures that the application that requested the authorization code is the same application that uses said code to get an access token. A random string called code verifier is used to create a code challenge. The authorization server must remember the code challenge. Then, when it receives a call to /oauth/token with the code verifier, it uses the same hash function to confirm that the code challenge is the same.
    • Client credentials: requires no end-user interaction because the user doesn’t own the resource.
    • Refresh token: makes sense only for the authorization code flow, to avoid having to request user consent every time.
    • Client device (not part of the spec): used for IoT, for example, a digital picture frame that requests permission to show pictures from another site. The user grants permission from a secondary device, like a phone.
    • Implicit flow: not recommended because it exposes access tokens in URL fragments which could be stored in the browser’s history.
Authorization Code with PKCE

OIDC

  • An authorization server can implement a /userinfo endpoint which returns claims with a JSON object. This is useful if the claims are too large to fit in an ID token, which is encoded as JWT. The /userinfo endpoint can only be accessed with a valid access token.
  • Three grant types / flows:
    • Authorization code flow: similar to OAuth 2’s flow.
    • Implicit flow: similar to OAuth2’s flow except that there is no risk of exposing an access token because the authorization server only returns an ID token in the form of a JWT.
    • Hybrid flow: mixture of the previous two. It is designed for applications with both a secure back end and a front-end.

SAML 2

  • Provides cross-domain single sign-on and identity federation (i.e. a way for an application and an identity provider to use a common shared identifier for a user).
  • The identity provider must implement a /sso endpoint where SAML requests can be received. The application must implement an /acs endpoint where the SAML response can be received.

Authorization

Authorization is the granting of privileges to access a resource. Access policy enforcement is done when a user requests a resource and a check is made to see if they can have access.

Three levels at which authorization may be specified and applied:

  1. At the application or API
  2. What functions can the user call in the application or API
  3. What data the user can access or operate on

Models for specifying authorization:

  • Access control lists
  • RBAC
  • ABAC
  • ReBAC
  • JWT-secured Authorization Requests (JARs), Rich Authorization Requests (RARs), or Pushed Authorization Requests (PARs).

Compliance

  • GDPR describes a legal basis for processing personal data. It applies to any product that processes personal information of EU residents, regardless of where the information is held.
  • Payment card industry must comply with PCI DSS.
  • HIPAA and HITECH is required for the healthcare industry in the United States.
  • FedRAMP applies to companies providing services to US government agencies.
  • SOC2 (Service Organization Control) is a set of controls against which a company is audited related to security, privacy, confidentiality, integrity and availability.
Solving Identity Management in Modern Applications – Book Notes

On doing a boomerang to AWS

A boomerang employee is someone who leaves a company and later returns to it. Most of the time it’s made to get a bump in salary without having to go through a promotion process.

I worked for Amazon as a software engineer for four years. I left in 2021.

Last week I got an email from a recruiter in AWS. The conversation can be summarized like this:

  • Recruiter: we miss you and surely you miss us! Do you want to boomerang back to AWS?
  • Me: Maybe, but I want no live coding interviews.
  • Recruiter: sorry, no.

I was… puzzled, to say the least.

Why title the email “do you want to boomerang?” if you are going to make me do interviews as if I were a brand new hire?

And why would you want to interview me as a brand new hire? Why do you need me to go through another coding interview? I publicly admit it, I hate those, for many reasons. But I worked for your company, you know I can code. And I left because I chose to, not because you decided suddenly that my code was not good.

Is it not in your best interest to make the process easier for me? Do you really prefer to hire someone completely new? Do you know how much time it takes a new hire to learn all of the amazon tools built in-house? I’ll tell you: it’s 6 months to a year at least.

The tech interview process is very broken…

On doing a boomerang to AWS