How to Get a Programming Job Straight Out of High School

While I was in High School I looked at the possible options ahead of me. The obvious option was to go onto college, but I was also looking at another option. I wanted to try to get a job working as a programmer and skip college altogether. Given the number of people that have skipped college and made their way directly into the tech industry, I’m surprised at how little there is written about what it’s like to go through the process. In this post, I explain, for people who are thinking about getting a job straight out of high school, what it’s like to go through the process and what obstacles you’ll run into along the way.

The first thing I will say is that it’s definitely possible to get a programming job without a college degree. I did it myself and wound up with offers from numerous companies including Google, Uber, and Dropbox, as well as several lesser known, smaller companies. Second, if you do decide to look for a job out of high school, I highly recommend applying to college and deferring for a year. Deferring will guarantee you have the option to go to school in case your job search doesn’t pan out.

As for actually getting a programming job straight out of high school, there are two main hurdles you will run into. First, you have to get companies to interview you. This stage will be especially tough for you. Second, after getting companies to interview you, you will be put into their interview processes. For most companies, the interview process consists of a several phone calls and an in person interview at the end. In each round you will be asked to solve one or more programming related problems. In most interviews, you will be asked to only describe a solution. In many of the interviews you also will be asked to write code for a working solution. Once you make it through a company’s interview process, you will find yourself with a job offer.

It’s important to emphasize that, for the most part, these two stages are completely separate. Once you get your foot in the door, how well your application looks does not matter. You could have the best application in the world, but if you fail a company’s interview they will still reject you.

Hurdle #1: Getting Companies to Interview You

There are many different approaches you can take to get companies to interview you and there is a lot of surprisingly bad advice out there. Here’s a list of several different approaches you can try and whether or not they work informed by my personal experience. Let’s start with the approaches that I found not to work:

What Does Not Work

Applying to a Company Directly

If you submit an application directly to a company, in most cases your resume will be sent to a recruiter. The recruiter will look at your resume and decide whether it’s worth the companies time to interview you. Usually when a recruiter looks at your application, they will first look for one of two things: a college degree from a well known school or prior experience at a well known tech company. Since you have neither of those things you are almost guaranteed to be rejected before you even start interviewing. You may hope that some company will take the risk and decide to interview you since you are straight out of high school, but as far as I know, no company actually does this.

When I tried applying directly to several companies, I did not get into the interview process for any of them. That was in spite of the fact that I had a Knuth check, several interesting side projects, and had won the Illinois statewide WYSE competition in Computer Science.

Contributing to Open Source

For some reason a lot of advice suggests that if you want to get a programming job, you should contribute to some open source software projects. As I see it, there are two things you can hope to gain out of working on open source:

  1. It looks good on your resume and you think it will help you pass the application process.
  2. You hope companies will see your open source contributions and reach out to you directly.

Regarding 1, as mentioned above, most recruiters first look for either a degree from a well known school or prior experience at a well known company. Without either of those, any open source contributions you have won’t matter. Most companies don’t even take a look at what open source projects you’ve worked on.

As for 2, in general companies only reach out to open source contributions if they are one of the core developers of a well known open source project. While it is possible to get interviews this way, the amount of effort you would have to put in is way more than it’s worth. The approaches I recommend later will help get you get interviews with a lot less effort.


As for approaches that actually work, here are two I’ve found that have worked. Both of them try to bypass the traditional resume screen and get you straight into companies’ interview processes.

What Does Work

Applying Through a Company that does Recruiting

There are a number of companies that will take care of the application process for you. Most of the job offers I got out of high school came out of the Recurse Center. The Recurse Center is a three month long program in New York where programmers work together on whatever projects they want. Although helping attendees find jobs isn’t the purpose of the Recurse Center, they do help all attendees looking for a job find one. After my time at the Recurse Center ended, I started looking for a job. The Recurse Center reached out to companies on my behalf and was able to get me straight into some companies’ interview pipelines. Even with the Recurse Center reaching out to companies on my behalf, I still only had ~1/3 companies decide to interview me, but that was enough for me to get several job offers.

Triplebyte is another option here. Triplebyte is a recruiting company that has their own interview process. If you pass their interview process, Triplebyte will then send you straight to the last round of interviews at other companies. If you are really good at technical interviews (described below), you should be able to pass Triplebyte’s interview process. Once you pass their interview, they will make sure you get interviews from other companies.

Networking

Another approach I’ve found successful is networking. If you have a friend at a company you want to work for and you can get them to refer you, they should be able to get you past the resume screen. Unfortunately, since you are straight out of high school, you probably don’t have much of a network so this option probably isn’t viable to you. If you do not already have a network, it is probably not worth building out a network just to get a job. In that case, you should try the approach above and apply through a company that handles the application process for you because it will take a lot less effort on your part.

Hurdle #2: Passing the Interview

Once you make it into a company’s interview process, all you need to do to get a job offer from them is to do well in their interview process. In general, programming interviews today across Most companies are very similar. Usually a company will ask you multiple algorithmic problems and you just need to be able to solve them to succeed in their interview. You may also be asked to write code that solves the problem.

An example of a question you will may run into is “Write a function that takes as input a number N and returns the number of different ways to make change for N cents.”

If you aren’t familiar with algorithm problems and how to solve them, there are tons of resources available. The book, Cracking the Coding Interview walks you through lots of different interview problems and how to solve them. If you want a comprehensive resource that covers everything you need to know, I really like the book Introduction to Algorithms. At over 1300 pages, it is a really long book, but it does cover everything you will need to know to solve algorithm related interview problems.

If you want to practice solving interview problems, the website LeetCode has many different algorithmic coding problems. The easy problems in the array section are about the difficulty you should expect in a programming interview. For any of those problems, you should be able to implement a program that solves the problem and be able to explain its runtime (in terms of big O notation) in <45 minutes.

In general, you should be familiar with the following:

  • What big O notation is and how to apply it. Given a piece of code or algorithm you should be able to easily determine its run time complexity and explain why it has that complexity.
  • All of the basic data structures (arrays, linked lists, hash tables, heaps, binary trees). For each data structure you should have all of the operations and the runtime of each operation memorized.
  • Basic algorithms (breadth first search, depth first search, quicksort, mergesort, binary search) and their runtimes.
  • Dynamic Programming. Dynamic programing is an algorithmic technique for solving various algorithms problems. I’m not sure why, but tons of companies ask problems that can be solved with dynamic programming.

There are a few companies that have started to move away from algorithm related interview problems, but the vast majority still ask them. You should mainly focus on learning how to solve algorithm problems and that should give you enough to pass the interview process at many different companies.


That’s really all there is to getting a job straight out of high school. It can all be boiled down to getting good at two things:  getting your foot in the door and getting really good at algorithm problems. If you are able to do both of these things, you will be able to start getting job offers fairly quickly. It isn’t impossible to get a programming job straight out of high school, you just need to work for it.

How to Improve Your Productivity as a Working Programmer

For the past few weeks, I’ve been obsessed with improving my productivity. During this time, I’ve continuously been monitoring the amount of work I’ve been getting done and have been experimenting with changes to make myself more productive. After only two months, I can now get significantly more work done than I did previously in the same amount of time.

If you had asked me my opinion on programmer productivity before I started this process, I wouldn’t have had much to say. After looking back and seeing how much more I can get done, I now think that understanding how to be more productive is one of the most important skills a programmer can have. Here are a few changes I’ve made in the past few weeks that have had a noticeable impact on my productivity:

Eliminating Distractions

One of the first and easiest changes I made was eliminating as many distractions as possible. Previously, I would spend a nontrivial portion of my day reading through Slack/email/Hacker News. Nearly all of that time could have been used much more effectively if I had only used that time to focus on getting my work done.

To eliminate as many distractions as possible, I first eliminated my habit of pulling out my phone whenever I got a marginal amount of work done. Now, as soon as I take my phone out of my pocket, I immediately put it back in. To make Slack less of a distraction, I left every Slack room that I did not derive immediate value from. Currently I’m only a in a few rooms that are directly relevant to my team and the work I do. In addition, I only allow myself to check Slack at specific times throughout the day. These times are before meetings, as well as before lunch and at the end of the day. I specifically do not check Slack when I first get into the office and instead immediately get started working.

Getting into the Habit of Getting into Flow

Flow is that state of mind where all of your attention is focused solely at the task at hand, sometimes referred to as “the zone”. I’ve worked on setting up my environment to maximize the amount of time I’m in flow. I moved my desk over onto the quiet side of the office and try set up long periods of time where I won’t be interrupted. When I want to get into flow, I’ll put on earmuffs, close all of my open tabs, and focus all of my energy at the task in front of me.

Scheduling My Day Around When I’m Most Productive

When I schedule my day, there are now two goals I have in mind. The first is to arrange all of my meetings together. This is to maximize the amount of time I can get into flow. The worst possible schedule I’ve encountered is having several meetings, all 30 minutes apart from each other. 30 minutes isn’t enough time for me to get any significant work done before being interrupted by my next meeting. Instead by aligning all of my meetings right next to each other, I go straight from one to the next. This way I have fewer larger blocks of time where I can get into flow and stay in flow.

The second goal I aim for is to arrange my schedule so I am working at the times of the day when I am most productive. I usually find myself most productive in the mornings. By the time 4pm rolls around, I am typically exhausted and have barely enough energy to get any work done at all. To reduce the effect this had on my productivity, I now schedule meetings specifically at the times of the day when I’m least productive. It doesn’t take a ton of energy to sit through a meeting, and scheduling my day this way allows me to work when I’m most productive. Think of it this way. If I can move a single 30 minute meeting from the time when I’m most productive to the time of the time at which I’m the least productive, I just added 30 minutes of productive time to my day.

Watching Myself Code

One incredibly useful exercise I’ve found is to watch myself program. Throughout the week, I have a program running in the background that records my screen. At the end of the week, I’ll watch a few segments from the previous week. Usually I will watch the times that felt like it took a lot longer to complete some task than it should have. While watching them, I’ll pay attention to specifically where the time went and figure out what I could have done better. When I first did this, I was really surprised at where all of my time was going.

For example, previously when writing code, I would write all my code for a new feature up front and then test all of the code collectively. When testing code this way, I would have to isolate which function the bug was in and then debug that individual function. After watching a recording of myself writing code, I realized I was spending about a quarter of the total time implementing the feature tracking down which functions the bugs were in! This was completely non-obvious to me and I wouldn’t have found it out without recording myself. Now that I’m aware that I spent so much time isolating which function a bugs are in, I now test each function as I write it to make sure they work. This allows me to write code a lot faster as it dramatically reduces the amount of time it takes to debug my code.

Tracking My Progress and Implementing Changes

At the end of every day, I spend 15 minutes thinking about my day. I think about what went right, as well as what went wrong and how I could have done better. At the end of the 15 minutes, I’ll write up my thoughts. Every Saturday, I’ll reread what I wrote for the week and implement changes based on any patterns I noticed.

As an example of a simple change that came out of this, previously on weekends I would spend an hour or two every morning on my phone before getting out of bed. That was time that would have been better used doing pretty much anything else. To eliminate that problem, I put my phone far away from my bed at night. Then when I wake up, I force myself to get straight into the shower without checking my phone. This makes it extremely difficult for me to waste my morning in bed on my phone, saving me several hours every week.

Being  Patient

I didn’t make all of these changes at once. I only introduced one or two of them at a time. If I had tried to implement all of these changes at once, I would have quickly burned out and given up. Instead, I was able to make a lot more changes by introducing each change more slowly. It only takes one or two changes each week for things to quickly snowball. After only a few weeks, I’m significantly more productive than I was previously. Making any progress at change at all is a lot better than no change. I think Stanford professor John Ousterhout’s quote describes this aptly. In his words, “a little bit of slope makes up for a lot of y-intercept”.

Time for An Intermission

I’ve been writing a lot these past two months. I decided I’m going to take a break for a little bit. I plan on starting to write continuously again within the next 2-4 weeks. That is all.

track_io_timing

The parameter track_io_timing is a relatively unknown, but super helpful parameter when optimizing queries. As the name suggests, when the parameter is turned on, Postgres will track how long I/O takes. Then, when you run a query with EXPLAIN (ANALYZE, BUFFERS), Postgres will display how much time was spent just performing I/O.

You normally don’t want to have track_io_timing always on since it incurs a significant amount of overhead. To get around this, when you want to time how long a query is spending performing I/O, you can use a transaction with SET LOCAL track_io_timing = on;. This will enable track_io_timing only during the transaction. As a specific example of track_io_timing, here’s a simple query over a table I have laying around:

> BEGIN; SET track_io_timing = ON; EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pets; COMMIT;
                                                QUERY PLAN                                                 
-----------------------------------------------------------------------------------------------------------
 Seq Scan on pets  (cost=0.00..607.08 rows=40008 width=330) (actual time=8.318..38.126 rows=40009 loops=1)
   Buffers: shared read=207
   I/O Timings: read=30.927
 Planning time: 161.577 ms
 Execution time: 42.104 ms

The I/O Timings field shows us that of the 42ms spent executing the query, ~31ms was spent performing I/O. Now if we perform the query again when the data is cached:

> BEGIN; SET track_io_timing = ON; EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pets; COMMIT;
                                                QUERY PLAN                                                
----------------------------------------------------------------------------------------------------------
 Seq Scan on pets  (cost=0.00..607.08 rows=40008 width=330) (actual time=0.004..7.504 rows=40009 loops=1)
   Buffers: shared hit=207
 Planning time: 0.367 ms
 Execution time: 11.478 ms

We can see the query is just about 31ms faster! This time the query does not show any information about the I/O timing since no time was spent performing I/O, due to the data being cached.

When benchmarking queries, I also make sure to make use of track_io_timing so I can see whether the expensive part of the query is performing I/O, or if the expensive part is something else entirely.

Postgres Upserts

Since Postgres 9.5, Postgres has supported a useful a feature called UPSERT. For a reason I can’t figure out, this feature is referred to as UPSERT, even though there is no UPSERT SQL command. In addition to being a useful feature, UPSERT is fairly interesting from a “behind the scenes” perspective as well.

If you haven’t noticed yet, the word “upsert” is a portmanteau of the words “update” and “insert”. As a feature, UPSERT allows you to insert a new data if that data does not already exist and specify an action to be performed instead if that data does already exist. More specifically, when there is a unique constraint on a column (a constraint specifying all values of a column are distinct from each other), UPSERT allow to say “insert this row if it does not violate the unique constraint, otherwise perform this action to resolve the conflict”.

As an example, let’s say we have a counters table where each row represents a counter. The table has two columns, id and value, where the id specifies the counter we are referring to, and value is the number of times the counter has been incremented. It would be nice if we could increment a counter without needing to create the counter in advance. This is a problem for UPSERT. First let’s create the table:

CREATE TABLE counters (id bigint UNIQUE, value bigint);

It’s important the the id column is marked as unique. Without that we would be unable to use UPSERT.

To write an UPSERT query, you first write a normal INSERT for the case when the constraint is not violated. In this case, when a counter with a given id does not already exist, we want to create a new counter with the given id and the value 1. An INSERT that does this looks like:

INSERT INTO counters (id, value)
SELECT <id> AS id, 1 AS value;

Then to make it an UPSERT, you add to the end of it ON CONFLICT (<unique column>) DO <action>. The action can either be NOTHING, in which case the query will be ignored, or it can be UPDATE SET <column1> = <expr1>, <column2> = <expr2> … This will modify the existing row and update the corresponding columns to the new values. In this case we want to use the UPDATE form to increment the value of the counter. The whole query winds up looking like:

INSERT INTO counters
SELECT <id> AS id, 0 AS value
    ON CONFLICT (id) DO 
    UPDATE SET value = counters.value + 1;

When you run the above command with a given id, it will create a new counter with the value 1 if a counter with the id does not already exist. Otherwise it will increment the value of the existing counter. Here’s some examples of its use:

> SELECT * FROM counters;
 id | value
----+-------
(0 rows)

> INSERT INTO counters
  SELECT 0 AS id, 1 AS VALUE
      ON CONFLICT (id) DO
      UPDATE SET value = counters.value + 1;

> SELECT * FROM counters;
 id | value
----+-------
  0 |     1
(1 row)

> INSERT INTO counters
  SELECT 0 AS id, 1 AS VALUE
      ON CONFLICT (id) DO
      UPDATE SET value = counters.value + 1;

> SELECT * FROM counters;
 id | value
----+-------
  0 |     2
(1 row)

> INSERT INTO counters
  SELECT 0 AS id, 1 AS VALUE
      ON CONFLICT (id) DO
      UPDATE SET value = counters.value + 1;

> SELECT * FROM counters;
 id | value
----+-------
  0 |     3
(1 row)

> INSERT INTO counters
  SELECT 1 AS id, 1 AS VALUE
      ON CONFLICT (id) DO
      UPDATE SET value = counters.value + 1;

> SELECT * FROM counters;
 id | value
----+-------
  0 |     3
  1 |     1
(2 rows)

> INSERT INTO counters
  SELECT 1 AS id, 1 AS VALUE
      ON CONFLICT (id) DO
      UPDATE SET value = counters.value + 1;

> SELECT * FROM counters;
 id | value
----+-------
  0 |     3
  1 |     2

One last bit about UPSERT, you can use the faux table excluded to refer to the new row being inserted. This is useful if you either want to values of the old row with values of the new row, or make the values of the row a combination of the values of the old and new rows. As an example, let’s say we want to extend the counter example to increment by an arbitrary amount. That can be done with:

INSERT INTO counters
SELECT <id> AS id, <amount> AS VALUE
    ON CONFLICT (id) DO
    UPDATE SET value = counters.value + excluded.value;

This even works if you are incrementing multiple counters simultaneously all by different amounts.

What makes UPSERT so interesting to me is that it works even in concurrent situations. UPSERT still works even if other INSERT and UPDATE queries are all running simultaneously! Prior to the UPSERT feature there was a fairly complex method to emulate UPSERT. That method involved using PL/pgSQL to alternate between running INSERT and UPDATE statements until one of them succeeded. The statements need to be ran in a loop because it is possible for a different INSERT to run before the UPSERT INSERT was ran, and a row could be deleted before the UPDATE could be ran. The UPSERT feature takes care of all of this for you, while at the same time providing a single command for the common pattern inserting data if it does not already exist and otherwise modifying the old data!

The Missing Postgres Scan: The Loose Index Scan

Postgres has many different types of scans builtin. The list includes sequential scans, index scans, and bitmap scans. One useful scan type Postgres does not have builtin that other databases do is the loose index scan. Both MySQL and Oracle support loose index scans. Fortunately for Postgres users, loose index scans can be emulated through a recursive CTE.

At a high level, the idea behind the loose index scan is that rows necessary for the query are at predictable locations in the index. All of the index scans Postgres currently supports requires reading all of the values from the index between two values. Although Postgres does not do it, for certain queries it is possible to skip over large parts of the index because it is possible to determine that values in that part of the index do not affect the result of the query. For a specific example, let’s say we want to calculate the number of distinct elements in a table:

CREATE TABLE ints (n BIGINT);

INSERT INTO ints SELECT floor(random() * 10) 
FROM generate_series(1, 10000000);

CREATE INDEX ON ints (n);

EXPLAIN ANALYZE SELECT COUNT(DISTINCT n) FROM ints;

This example creates a table with 10,000,000 rows, all of which contain a random number from 0 to 9. Then it counts the number of distinct elements using a naive COUNT(DISTINCT n). The plan for the query is available here.

As you can see from the query plan, using a naive COUNT(DISTINCT n) takes six seconds. Of this time, 650 milliseconds is used to read the data from disk while the rest of the time is used to perform the aggregation and determine how many distinct elements there are. A sequential scan is only performed because the query requires reading all of the rows from the database and a sequential scan is the fastest way to do it.

Most of the work Postgres is doing for this query is unnecessary. For example, once Postgres sees a row with a zero, it knows all other rows that contain a zero will not affect the result. With this idea in mind, we can write the following query which performs a loose index scan:

> EXPLAIN ANALYZE
> WITH RECURSIVE temp (i) AS (
>     (SELECT n FROM ints ORDER BY n ASC LIMIT 1)
>   UNION ALL
>     (SELECT n FROM temp, 
>                    LATERAL (SELECT n
>                             FROM ints
>                             WHERE n > i
>                             ORDER BY n ASC
>                             LIMIT 1) sub)
> )
> SELECT COUNT(*) FROM temp;

The plan is available here.

This new query takes less than one millisecond! Let’s try dissecting this query. First of all, this query makes use of a recursive CTE. If you aren’t familiar with how to interpret recursive CTEs, I suggest you read my blog post on how to understand them. The first part of the recursive CTE is:

SELECT n FROM ints ORDER BY n ASC LIMIT 1

This simply gets the smallest number from the table. As for the recursive part of the recursive CTE:

SELECT n FROM temp, 
              LATERAL (SELECT n
                       FROM ints
                       WHERE n > i
                       ORDER BY n ASC
                       LIMIT 1) sub

This query takes the current element in the working table and uses a lateral join to fetch the smallest element greater than the current element. In other words, the next largest element. By first fetching the smallest value in the table and then repeatedly fetching the next largest, the recursive CTE selects the distinct elements from the table! From there, we just need to count the number of distinct elements to determine the number of distinct elements in the table.

By using an index to find each element, only a single index scan needs to be performed per distinct element. Since there are only 10 distinct elements in the table, only 10 index scans need to be performed1. The loose index scan is so much faster than the regular aggregation because once it sees a given value, it skips over all other rows with the same value.

Besides counting the number of distinct elements in a table, there is one other main use case where loose index scans are helpful: if you ever want to calculate the min of one column grouped by another column. If you have a specific value of the column you are grouping by, you can use a compound index on (grouping column, other column) to quickly find the smallest value of the other column for that one value. By iterating over the distinct values of the column you are grouping by, you can answer the query fairly quickly. Without using a loose index scan here, the query would read entire table, which as we already know, is pretty slow.