<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ClassDojo Engineering Blog]]></title><description><![CDATA[Welcome to the ClassDojo Engineering Blog]]></description><link>https://engineering.classdojo.com</link><image><url>/classdojo.jpg</url><title>ClassDojo Engineering Blog</title><link>https://engineering.classdojo.com</link></image><generator>GatsbyJS</generator><lastBuildDate>Mon, 01 Dec 2025 14:52:51 GMT</lastBuildDate><atom:link href="https://engineering.classdojo.com/rss" rel="self" type="application/rss+xml"/><pubDate>Mon, 01 Dec 2025 14:50:18 GMT</pubDate><language><![CDATA[en]]></language><item><title><![CDATA[Automating Weekly Business Reviews with DBT Macros]]></title><description><![CDATA[Authors: [An Le](https://www.linkedin.com/in/anthaile/), [Evan Jones](https://www.linkedin.com/in/evan-h-jones/)]]></description><link>https://engineering.classdojo.com/2025/03/19/automating-weekly-business-reviews-with-dbt</link><guid isPermaLink="true">https://engineering.classdojo.com/2025/03/19/automating-weekly-business-reviews-with-dbt</guid><category><![CDATA[Metrics]]></category><category><![CDATA[Data]]></category><category><![CDATA[dbt]]></category><dc:creator><![CDATA[Evan Jones]]></dc:creator><pubDate>Wed, 19 Mar 2025 00:00:00 GMT</pubDate></item><item><title><![CDATA[XKCD's "Is It Worth the Time?" Considered Harmful]]></title><description><![CDATA[!["Is It Worth the Time?"](https://imgs.xkcd.com/comics/is_it_worth_the_time.png)]]></description><link>https://engineering.classdojo.com/2025/01/08/its-not-worth-the-time-yet</link><guid isPermaLink="true">https://engineering.classdojo.com/2025/01/08/its-not-worth-the-time-yet</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Wed, 08 Jan 2025 00:00:00 GMT</pubDate></item><item><title><![CDATA[18,957 tests in under 6 minutes: ClassDojo's approach to backend testing]]></title><description><![CDATA[We're pretty proud of our backend test suite. We have a lot of tests, and developers can run the full test suite locally in under six minutes. These aren't simple unit tests—they're tests that hit multiple databases and routes with only a minimal amount of stubbing for external dependencies.]]></description><link>https://engineering.classdojo.com/2024/12/13/backend-testing-at-classdojo-2024</link><guid isPermaLink="true">https://engineering.classdojo.com/2024/12/13/backend-testing-at-classdojo-2024</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Fri, 13 Dec 2024 00:00:00 GMT</pubDate></item><item><title><![CDATA[JSDoc comments can make fixtures easier to work with]]></title><description><![CDATA[![Image of test file with a fixtureId imported. The editor is hovering over fixtureId.teacher1Id which shows a wealth of information about which classes, students, parents, and schools that teacher is connected to.](https://static.classdojo.com/images/jsdocFixtureId.png)]]></description><link>https://engineering.classdojo.com/2024/10/30/jsdoc-comments-for-fixtures</link><guid isPermaLink="true">https://engineering.classdojo.com/2024/10/30/jsdoc-comments-for-fixtures</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Wed, 30 Oct 2024 00:00:00 GMT</pubDate></item><item><title><![CDATA[Adopting React Query]]></title><description><![CDATA[## Intro

In my first week at ClassDojo, a small bug fix I was working on presented an intriguing
opportunity. A UI component was displaying stale data after an update due to a bug within a
ClassDojo library named Fetchers, a React Hooks implementation for fetching and managing
server state on the client. I couldn't help but ask “Why do we have a custom React Query
lookalike in our codebase?” A quick peek at the Git history revealed that Fetchers predate
React Query and many other similar libraries, making it our best option at the time. Four years
have passed since then and we now have several other options to consider. We _could_ stick with
Fetchers, but why not use an established library that does the _exact_ same thing? After
discussing the tradeoffs with other engineers, it became apparent that React Query was a better
fit for our codebase. With a vision set, we formulated a plan to migrate from Fetchers to React
Query.

## The Tradeoffs, or Lack Thereof

Like most engineering problems, deciding between Fetchers and React Query meant evaluating
some tradeoffs. With Fetchers, we had complete control and flexibility over the API, designing it
directly against our use cases. With React Query, we would have to relinquish control over the
API and adapt to its interface. What ended up being a small downgrade in flexibility was a huge
upgrade in overall cost. Maintaining Fetchers involved time & effort spent writing, evolving,
debugging, testing, and documenting the library, and that was not cheap. Fortunately, React
Query supports all the existing use cases that Fetchers did _and then some_, so we’re not really
giving up anything.

As if that wasn't enough to convince us, Fetchers also had a few downsides that were crucial in
our decision-making. The first was that Fetchers was built on top of redux, a library we’re
actively working at removing from our codebase (for other unrelated reasons). The second, due
to the first, is that Fetchers didn’t support callbacks or promises for managing the lifecycle of
mutations. Instead, we only returned the status of a mutation through the hook itself. Often, prop
drilling would separate the mutation function from the status props, splitting the mutation trigger
and result/error handling across separate files. Sometimes the status props were ignored
completely since it wasn’t immediately obvious if a mutation already had handling set up
elsewhere.

```typescript jsx
// Fetchers Example

// Partial typing to illustrate the point
type FetcherResult<Params> = {
  start: (params: Params) => void;
  done: boolean;
  error: Error;
}

// More on this later...
const useFetcherMutation = makeOperation({ ... });

const RootComponent = () => {
  const { start, done, error }: FetcherResult = useFetcherMutation();

  useEffect(() => {
    if (error) {
      // handle error
    } else if (done) {
      // handle success
    }
  }, [done, error]);

  return (
    <ComponentTree>
      <LeafComponent start={start} />
    </ComponentTree>
  )
}

const LeafComponent = ({ start }) => {
  const handleClick = () => {
    // No way to handle success/error here, we can only call it.
    // There may or may not be handling somewhere else...?
    start({ ... });
  };

  return <button onClick={start}>Start</button>;
}
```

With React Query, the mutation function itself allows for handling the trigger & success/error
cases co-located:

```typescript jsx
// React Query

// Partial typing to illustrate the point
type ReactQueryResult<Params, Result> = {
  start: (params: Params) => Promise<Result>;
}

// More on this later...
const useReactQueryMutation = makeOperation({ ... });

const RootComponent = () => {
  const { start }: FetcherResult = useReactQueryMutation();

  return (
    <ComponentTree>
      <LeafComponent start={start} />
    </ComponentTree>
  )
}

const LeafComponent = ({ start }) => {
  // Mutation trigger & success/error cases colocated
  const handleClick = async () => {
    try {
      const result = await start({ ... });
      // handle success
    } catch(ex) {
      // handle error
    }
  }

  return <button onClick={handleClick}>Start</button>;
}
```

Finally, Fetchers’ cache keys were string-based, which meant they couldn’t provide granular
control for targeting multiple cache keys like React Query does. For example, a cache key’s
pattern in Fetchers looked like this:

```javascript
const cacheKey = 'fetcherName=classStoryPost/params={"classId":"123","postId":"456"}'
```

In React Query, we get array based cache keys that support objects, allowing us to target
certain cache entries for invalidation using partial matches:

```javascript
const cacheKey = ['classStoryPost', { classId: '123', postId: '456' }];

// Invalidate all story posts for a class
queryClient.invalidateQueries({ queryKey: ['classStoryPost', { classId: '123' }] });
```

The issues we were facing were solvable problems, but not worth the effort. Rather than
continuing to invest time and energy into Fetchers, we decided to put it towards migrating our
codebase to React Query. The only question left was “How?”

## The Plan

At ClassDojo, we have a weekly “web guild” meeting for discussing, planning, and assigning
engineering work that falls outside the scope of teams and their product work. We used these
meetings to drive discussions and gain consensus around a migration plan and divvy out the
work to developers.

To understand the plan we agreed on, let’s review Fetchers. The API consists of three primary
functions: `makeMemberFetcher`, `makeCollectionFetcher`, and `makeOperation`. Each is
a factory function for producing hooks that query or mutate our API. The hooks returned by each
factory function are _almost_ identical to React Query’s `useQuery`, `useInfiniteQuery`, and
`useMutation` hooks. Functionally, they achieve the same things, but with different options,
naming conventions, and implementations. The similarities between the hooks returned from
Fetchers’ factory functions and React Query made for the perfect place to target our migration.

The plan was to implement alternate versions of Fetcher’s factory functions using the same API
interfaces, but instead using React Query hooks under the hood. By doing so, we could ship
both implementations simultaneously and use a feature switch to toggle between the two.
Additionally, we could rely on Fetchers’ unit tests to catch any differences between the two.

![](https://directus.internal.classdojo.com/assets/66bec141-fd42-4d63-a112-dc6b2dc0d77f)

Our plan felt solid, but we still wanted to be careful in how we rolled out the new
implementations so as to minimize risk. Given that we were rewriting each of Fetchers’ factory
functions, each had the possibility of introducing their own class of bugs. On top of that, our
front end had four different apps consuming the Fetchers library, layering on additional usage
patterns and environmental circumstances. Spotting errors thrown inside the library code is
easy, but spotting errors that cascade out to other parts of the app as a result of small changes
in behavior is _much_ harder. We decided to use a phased rollout of each factory function one at a
time, app by app so that any error spikes would be isolated to one implementation or app at a
time, making it easy to spot which implementation had issues. Below is some pseudocode that
illustrates the sequencing of each phase:

```
for each factoryFn in Fetchers:
  write factoryFn using React Query
  for each app in ClassDojo:
    rollout React Query factoryFn using feature switch
    monitor for errors
    if errors:
      turn off feature switch
      fix bugs
      repeat
```

## What Went Well?

Abstractions made the majority of this project a breeze. The factory functions provided a single
point of entry to replace our custom logic with React Query hooks. Instead of having to assess
all 365 usages of Fetcher hooks, their options, and how they map to a React Query hook, we
just had to ensure that the hook _returned_ by each factory function behaved the same way it did
before. Additionally, swapping implementations between Fetchers and React Query was just a
matter of changing the exported functions from Fetchers’ index file, avoiding massive PRs with
100+ files changed in each:

```typescript
// before migration

export { makeMemberFetcher } from './fetchers';

// during migration

import { makeMemberFetcher as makeMemberFetcherOld } from './fetchers';
import { makeMemberFetcher as makeMemberFetcherNew } from './rqFetchers';

const makeMemberFetcher = isRQFetchersOn ? makeMemberFetcherNew : makeMemberFetcherOld;

export { makeMemberFetcher };
```

Our phased approach played a big role in the success of the project. The implementation of
makeCollectionFetcher worked fine in the context of one app, but surfaced some errors in the
context of another. It wasn’t necessarily easy to know _what_ was causing the bug, but the surface
area we had to scan for potential problems was _much_ smaller, allowing us to iterate faster.
Phasing the project also naturally lent itself well to parallelizing the development process and
getting many engineers involved. Getting the implementations of each factory function to
behave exactly the same as before was not an easy process. We went through many iterations
of fixing broken tests before the behavior matched up correctly. Doing that alone would have
been a slow and painful process.

## How Can We Improve?

One particular pain point with this project were Fetchers’ unit tests. Theoretically, they should
have been all we needed to verify the new implementations. Unfortunately, they were written
with dependencies on implementation details, making it difficult to just run them against a new
implementation. I spent some time trying to rewrite them, but quickly realized the effort wasn't
worth the payoff. Instead, we relied on unit & end-to-end tests throughout the application that
would naturally hit these codepaths. The downside was that we spent a lot of time stepping
through and debugging those other tests to understand what was broken in our new
implementations. This was a painful reminder to write unit tests that only observe the inputs and
outputs.

Another pain point was the manual effort involved in monitoring deployments for errors. When
we rolled out the first phase of the migration, we realized it’s not so easy to tell whether we were
introducing new errors or not. There was a lot of existing noise in our logs that required
babysitting the deployments and checking reported errors to confirm whether or not the error
was new. We also realized we didn’t have a good mechanism for scoping our error logs down to
the latest release only. We’ve since augmented our logs with better tags to make it easier to
query for the “latest” version. We’ve also set up a weekly meeting to triage error logs to specific
teams so that we don’t end up in the same situation again.

## What's Next?

Migrating to React Query was a huge success. It rid us of maintaining a complex chunk of code
that very few developers even understood. Now we’ve started asking ourselves, “What’s next?”.
We’ve already started using lifecycle callbacks to deprecate our cache invalidation & optimistic
update patterns. Those patterns were built on top of redux to subscribe to lifecycle events in
Fetchers’ mutations, but now we can simply hook into onMutate, onSuccess, onError provided
by React Query. Next, we’re going to look at using async mutations to simplify how we handle
the UX for success & error cases. There are still a lot of patterns leftover from Fetchers and it
will be a continued effort to rethink how we can simplify things using React Query.

## Conclusion

Large code migrations can be really scary. There’s a lot of potential for missteps if you’re not
careful. I personally believe that what made this project successful was treating it like a refactor.
The end goal wasn’t to _change_ the behavior of anything, just to refactor the implementation.
Trying to swap one for the other without first finding their overlap could have made this a messy
project. Instead, we wrote new implementations, verified they pass our tests, and shipped them
one by one. This project also couldn’t have happened without the excellent engineering culture
at ClassDojo. Instead of being met with resistance, everyone was eager and excited to help out
and get things moving. I’m certain there will be more projects like this to follow in the future.]]></description><link>https://engineering.classdojo.com/2023/09/11/adopting-react-query</link><guid isPermaLink="true">https://engineering.classdojo.com/2023/09/11/adopting-react-query</guid><dc:creator><![CDATA[Tim Pace]]></dc:creator><pubDate>Mon, 11 Sep 2023 00:00:00 GMT</pubDate></item><item><title><![CDATA[Slack is the Worst Info-Radiator]]></title><description><![CDATA[When the ClassDojo engineering team was in the office, we loved our information radiators: we had multiple huge monitors showing broken jenkins builds, alerts, and important performance statistics. They worked amazingly well for helping us keep our CI/CD pipelines fast & unblocked, helped us keep the site up & fast, and helped us build an engineering culture that prioritized the things we showed on the info radiators. They worked well while the whole team was in the office, but when we went fully remote, our initial attempt of moving that same information into a slack channel failed completely, and we had to find a different way to get the same value.]]></description><link>https://engineering.classdojo.com/2023/06/14/slack-is-the-worst-info-radiator</link><guid isPermaLink="true">https://engineering.classdojo.com/2023/06/14/slack-is-the-worst-info-radiator</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Wed, 14 Jun 2023 00:00:00 GMT</pubDate></item><item><title><![CDATA[Being On-call at ClassDojo]]></title><description><![CDATA[Having a software engineer on-call at a tech company ensures that if any failures or alerts occur, there is someone dedicated to respond to them. Most engineers will have experienced some form of being on-call over their career. I have seen many posts on Hacker News about being on-call with negative opinions and poor experiences. Thankfully, I cannot commiserate with these examples, so my goal of this post is to show how we have tried to reduce the pain points of being on-call at ClassDojo.]]></description><link>https://engineering.classdojo.com/2023/02/22/oncall-at-classdojo</link><guid isPermaLink="true">https://engineering.classdojo.com/2023/02/22/oncall-at-classdojo</guid><dc:creator><![CDATA[Sarah Mahovlich]]></dc:creator><pubDate>Wed, 22 Feb 2023 00:00:00 GMT</pubDate></item><item><title><![CDATA[Culling Containers with a Leaky Bucket]]></title><description><![CDATA[ClassDojo occasionally has a few containers get into bad states that they're not able to recover from. This normally happens when a connection for a database gets into a bad state -- we've seen this with Redis, MySQL, MongoDB, and RabbitMQ connections. We do our best to fix these problems, but we also want to make it so that our containers have a chance of recovering on their own without manual intervention. We don't want to wake people up at night if we don't need to! Our main strategy to make that happen is having our containers decide whether they should try restarting themselves.]]></description><link>https://engineering.classdojo.com/2022/12/05/culling-containers-with-a-leaky-bucket</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/12/05/culling-containers-with-a-leaky-bucket</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Mon, 05 Dec 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Patterns of the Swarm]]></title><description><![CDATA[On our teams, we do our best to ensure that we're fully focused on the most important thing that our team could be doing. To do that, we often "swarm" on the top priority: this is some internal documentation on what that looks like in practice!]]></description><link>https://engineering.classdojo.com/2022/11/22/patterns-of-the-swarm</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/11/22/patterns-of-the-swarm</guid><dc:creator><![CDATA[Gregg Caines]]></dc:creator><pubDate>Tue, 22 Nov 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Shell Patterns for Easy Code Migrations]]></title><description><![CDATA[Automated and semi-automated code migrations using shell text manipulation tools are great! Turning a migration task that might take multiple days or weeks of engineering effort into one that you can accomplish in a few minutes can be a huge win. I'm not remotely an expert at these migrations, but I thought it'd still be useful to write up the patterns that I use consistently.]]></description><link>https://engineering.classdojo.com/2022/10/12/shell-patterns-for-easy-code-migrations</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/10/12/shell-patterns-for-easy-code-migrations</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Wed, 12 Oct 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Simulating network problems with docker network]]></title><description><![CDATA[TL;DR: see how your code responds to not being able to reach a dependency]]></description><link>https://engineering.classdojo.com/2022/10/05/simulating-network-problems-with-docker-network</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/10/05/simulating-network-problems-with-docker-network</guid><dc:creator><![CDATA[Nick Bottomley]]></dc:creator><pubDate>Wed, 05 Oct 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Post-Merge Code Reviews are Great!]]></title><description><![CDATA[One aspect of our workflow that people often find odd or new to them is that we don’t require an up-front code review or pull request with signoff from another engineer before pushing to production. This often comes up in interviews or conversations with engineers who are interested in jobs at ClassDojo, with reactions ranging from curiosity to aversion.]]></description><link>https://engineering.classdojo.com/2022/09/14/post-merge-code-reviews-are-great</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/09/14/post-merge-code-reviews-are-great</guid><dc:creator><![CDATA[Victor Essnert]]></dc:creator><pubDate>Wed, 14 Sep 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Engineering Dojo, Episode 2: How and why we use CI/CD]]></title><description><![CDATA[One of the main engineering backbones here are ClassDojo is our CI/CD pipeline. By having a pipeline that automates tests, migrations, rolling out to production, and so much more, we allow the engineering team to focus on building great products. No managing a release train or babysitting a deployment!]]></description><link>https://engineering.classdojo.com/2022/08/24/enginering-dojo-2-ci-cd</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/08/24/enginering-dojo-2-ci-cd</guid><category><![CDATA[Engineering Dojo Podcast]]></category><dc:creator><![CDATA[Andrew Burgess]]></dc:creator><pubDate>Wed, 24 Aug 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Playtesting: maintaining high product quality without QA]]></title><description><![CDATA[One aspect of ClassDojo’s engineering culture that tends to surprise people is that we don’t have dedicated QA engineers or testers. We use a variety of strategies to keep our end-to-end feature development fast, continuous, and high quality. These strategies include a strong culture of writing automated tests, along with regular playtesting sessions. ]]></description><link>https://engineering.classdojo.com/2022/08/24/playtesting</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/08/24/playtesting</guid><dc:creator><![CDATA[Melissa Dirdo]]></dc:creator><pubDate>Wed, 24 Aug 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Using NodeJS's AsyncLocalStorage to Instrument a Webserver]]></title><description><![CDATA[In the dark times before [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), it could be hard to tell why a request would occasionally time out. Were there multiple relatively slow queries somewhere in that route's code? Was another request on the same container saturating a database connection pool? Did another request block the event loop? It was possible to use tracing, profiling, and logs to track down problems like these, but it could be tricky; setting up per route metrics using `AsyncLocalStorage` makes it a ton easier!]]></description><link>https://engineering.classdojo.com/2022/08/09/async-local-storage-in-detail</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/08/09/async-local-storage-in-detail</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Tue, 09 Aug 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Large Analytics SQL Queries are a Code Smell]]></title><description><![CDATA[A single large query in SQL can be hard to understand, test, debug, or change in the same way that an over-large function in code can be. A large query is also much harder to write! Feedback loops while writing large queries are slow and you'll often find yourself needing to guess at where the problem in your query is.]]></description><link>https://engineering.classdojo.com/2022/08/03/large-analytics-sql-queries-are-a-code-smell</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/08/03/large-analytics-sql-queries-are-a-code-smell</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Wed, 03 Aug 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[From Google Analytics to Matomo Part 3]]></title><description><![CDATA[In [Part 1](https://engineering.classdojo.com/2022/07/13/google-analytics-to-matomo-p1) we talked about why we switched from Google Analytics to Matomo.  In [Part 2](https://engineering.classdojo.com/2022/07/13/google-analytics-to-matomo-p2), we discussed how we designed the architecture.  Finally, here in Part 3 we will look at the Matomo specific changes necessary to support our architecture.]]></description><link>https://engineering.classdojo.com/2022/07/27/google-analytics-to-matomo-p3</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/07/27/google-analytics-to-matomo-p3</guid><dc:creator><![CDATA[Dominick Bellizzi]]></dc:creator><pubDate>Wed, 27 Jul 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[From Google Analytics to Matomo Part 2]]></title><description><![CDATA[In [Part 1](https://engineering.classdojo.com/2022/07/13/google-analytics-to-matomo-p1), we discussed why we were moving from Google analytics to Matomo.  Now, in Part 2, we’ll talk about how we architected Matomo to handle traffic at ClassDojo’s scale.]]></description><link>https://engineering.classdojo.com/2022/07/20/google-analytics-to-matomo-p2</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/07/20/google-analytics-to-matomo-p2</guid><dc:creator><![CDATA[Dominick Bellizzi]]></dc:creator><pubDate>Wed, 20 Jul 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[From Google Analytics to Matomo Part 1]]></title><description><![CDATA[ClassDojo is committed to keeping the data from our teachers, parents, and students secure and private.  We have a few principles that guide how we handle user data:]]></description><link>https://engineering.classdojo.com/2022/07/13/google-analytics-to-matomo-p1</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/07/13/google-analytics-to-matomo-p1</guid><dc:creator><![CDATA[Dominick Bellizzi]]></dc:creator><pubDate>Wed, 13 Jul 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Scalable, Replicated VPN - with Pritunl]]></title><description><![CDATA[VPCs are a pretty ubiquitous piece of cloud infrastructure for most tech stack these days. It is an important to get the IP address layout of your Virtual Private Cloud (VPC) right from the start. With far-reaching implications for scaling, fault-tolerance and security, a VPC is a great tool to separate out the wild-west of the web and your precious servers. However, once you have a VPC, you’ll need the ability to access your VPC protected servers. This is where VPNs come in.]]></description><link>https://engineering.classdojo.com/2022/06/12/scalable-replicated-vpn-with-pritunl</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/06/12/scalable-replicated-vpn-with-pritunl</guid><category><![CDATA[Engineering]]></category><category><![CDATA[Programming]]></category><category><![CDATA[Networking]]></category><dc:creator><![CDATA[Urjit Singh Bhatia]]></dc:creator><pubDate>Sun, 12 Jun 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[TypeScript String Theory]]></title><description><![CDATA[TypeScript brings power to even the humble string. Let's take a look at several ways we incorporate better string-based types into our code.]]></description><link>https://engineering.classdojo.com/2022/05/04/typescript-string-theory</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/05/04/typescript-string-theory</guid><dc:creator><![CDATA[Andrew Burgess]]></dc:creator><pubDate>Wed, 04 May 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Engineering Dojo, Episode 1: Building Payment Systems]]></title><description><![CDATA[As you might know, ClassDojo offers a paid subscription for parents, to give them extra tools for interacting with both their teachers and their kids. For several years, we built and supported a system for managing those subscriptions. That’s non-trival work: it involves integrations with several app stores and payment providers and wrangling different data models for each.]]></description><link>https://engineering.classdojo.com/2022/04/05/engineering-dojo-1-payment-system</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/04/05/engineering-dojo-1-payment-system</guid><category><![CDATA[Engineering Dojo Podcast]]></category><dc:creator><![CDATA[Andrew Burgess]]></dc:creator><pubDate>Tue, 05 Apr 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Red and Blue Function Mistakes in JavaScript]]></title><description><![CDATA[Bob Nystrom's [What Color is Your Function](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/) does an amazing job of describing why it can be painful when programming languages have different rules for calling synchronous and asynchronous functions. Promises and async/await have simplified things in JavaScript, but it's still a language with "red" (async) and "blue" (sync) functions, and I consistently see a few understandable errors from red vs. blue function confusion. Let's go through some of the most common mistakes – none of these are bad things to get wrong, they're just a symptom of how confusing this can be!]]></description><link>https://engineering.classdojo.com/2022/03/25/red-blue-function-mistakes-in-js</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/03/25/red-blue-function-mistakes-in-js</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Fri, 25 Mar 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Staying Connected as a Digital Nomad]]></title><description><![CDATA[Since the dawn of the internet, software engineers have found ways to effectively collaborate remotely, and over the years we've become quite good at it. If you need convincing, take a look at any large-scale open source project, and you'll find that it was likely created by people from different countries, time zones, and languages, all collaborating on a single project and working toward a shared vision.]]></description><link>https://engineering.classdojo.com/2022/03/10/staying-connected-as-a-digital-nomad</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/03/10/staying-connected-as-a-digital-nomad</guid><dc:creator><![CDATA[Tyler Eastman]]></dc:creator><pubDate>Thu, 10 Mar 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[How We Built a DataOps Platform]]></title><description><![CDATA[[DataOps](https://dataopsmanifesto.org/en/) has always been the vision for the ClassDojo team, even if we didn’t think of it using that term. Our goal has been to give every vertically integrated team ownership over its own metrics, tables, and data dictionaries, and the processes for generating those artifacts. These things should be written mostly in code, with some generated documentation.]]></description><link>https://engineering.classdojo.com/2022/02/07/dataops-at-classdojo</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/02/07/dataops-at-classdojo</guid><dc:creator><![CDATA[Felix Yuan]]></dc:creator><pubDate>Mon, 07 Feb 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Editing 200 Files with Bash and Perl]]></title><description><![CDATA[I recently had to change 189 files in our code base, all in almost the same way. Rather than doing it manually, I decided to brush up on my command-line text manipulation ... and ended up taking it further than I expected.]]></description><link>https://engineering.classdojo.com/2022/01/13/editing-200-files-with-bash-and-perl</link><guid isPermaLink="true">https://engineering.classdojo.com/2022/01/13/editing-200-files-with-bash-and-perl</guid><dc:creator><![CDATA[Andrew Burgess]]></dc:creator><pubDate>Thu, 13 Jan 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Our Approach to Mob Programming]]></title><description><![CDATA[Our teams at ClassDojo have the freedom to choose how they want to work. Many of our teams have started spending a few hours each day mobbing because we've found it to be an effective form of collaboration. Here's how we do it!]]></description><link>https://engineering.classdojo.com/2021/12/06/mob-programming-at-classdojo</link><guid isPermaLink="true">https://engineering.classdojo.com/2021/12/06/mob-programming-at-classdojo</guid><dc:creator><![CDATA[Melissa Dirdo]]></dc:creator><pubDate>Mon, 06 Dec 2021 00:00:00 GMT</pubDate></item><item><title><![CDATA[Canary Containers at ClassDojo in Too Much Detail]]></title><description><![CDATA[[Canary releases](https://martinfowler.com/bliki/CanaryRelease.html) are pretty great! ClassDojo uses them as part of our continuous delivery pipeline: having a subset of real users use & validate our app before continuing with deploys allows us to safely & automatically deploy many times a day.]]></description><link>https://engineering.classdojo.com/2021/11/02/canary-containers-in-too-much-detail</link><guid isPermaLink="true">https://engineering.classdojo.com/2021/11/02/canary-containers-in-too-much-detail</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Tue, 02 Nov 2021 00:00:00 GMT</pubDate></item><item><title><![CDATA[Entity Component Systems: Performance isn't the only benefit]]></title><description><![CDATA[Many companies, like [Unity](https://docs.unity3d.com/Packages/com.unity.entities@0.17/manual/index.html) and [Apple](https://developer.apple.com/library/archive/documentation/General/Conceptual/GameplayKit_Guide/EntityComponent.html#//apple_ref/doc/uid/TP40015172-CH6-SW1) are using Entity Component Systems (ECS) to make games because having tightly packed data leads to cache efficiency and great performance. ECS isn’t just great for performance though: it leads to good code-decoupling, easy composition, and lends itself to TDD and automated testing.]]></description><link>https://engineering.classdojo.com/2021/10/29/entity-component-systems-lead-to-great-code</link><guid isPermaLink="true">https://engineering.classdojo.com/2021/10/29/entity-component-systems-lead-to-great-code</guid><dc:creator><![CDATA[Michael Graves]]></dc:creator><pubDate>Fri, 29 Oct 2021 00:00:00 GMT</pubDate></item><item><title><![CDATA[P1, P2,...P5 is a broken system: here's what we do instead]]></title><description><![CDATA[Software developers need to prioritize bugs when they are discovered, and most do this with an ordered priority scheme. Tracking tools like Jira provide a column on each ticket, and many organizations use numbers, such as P1, P2, P3, P4, and P5. Some even adopt a P0 as an indicator that there are some bugs that have an even higher priority than P1. Often there will be criteria for each of them, for example “all issues preventing users from logging in are P1.”]]></description><link>https://engineering.classdojo.com/2021/09/30/p1-p2-is-a-broken-system</link><guid isPermaLink="true">https://engineering.classdojo.com/2021/09/30/p1-p2-is-a-broken-system</guid><dc:creator><![CDATA[Dominick Bellizzi]]></dc:creator><pubDate>Thu, 30 Sep 2021 00:00:00 GMT</pubDate></item><item><title><![CDATA[Even Better Rate Limiting]]></title><description><![CDATA[You may have read our post from a few years ago [implementing a rolling-window rate limiter](https://engineering.classdojo.com/2015/02/06/rolling-rate-limiter), where we talked about the implementation of our sliding log rate limiter. That approach has been working well since then, but with increases in traffic, we recently found ourselves rebuilding the rate limiter to improve performance. With some relatively small changes, we were able to improve our redis CPU usage by 40x!]]></description><link>https://engineering.classdojo.com/2021/08/25/even-better-rate-limiting</link><guid isPermaLink="true">https://engineering.classdojo.com/2021/08/25/even-better-rate-limiting</guid><dc:creator><![CDATA[Andrew Burgess]]></dc:creator><pubDate>Wed, 25 Aug 2021 00:00:00 GMT</pubDate></item><item><title><![CDATA[AsyncLocalStorage Makes the Commons Legible]]></title><description><![CDATA[When multiple teams share a resource like a code-base, a database, or an analytics pipeline, that resource can often suffer from the _Tragedy of the Commons_. Well intentioned engineers can add slow code, not prioritize important improvements because those improvements aren't aligned with team incentives, and just generally not leave the codebase or database better than they found it.]]></description><link>https://engineering.classdojo.com/2021/08/20/asynclocalstorage-makes-the-commons-legible</link><guid isPermaLink="true">https://engineering.classdojo.com/2021/08/20/asynclocalstorage-makes-the-commons-legible</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Fri, 20 Aug 2021 00:00:00 GMT</pubDate></item><item><title><![CDATA[The 3 things I didn't understand about TypeScript]]></title><description><![CDATA[When the ClassDojo engineering team started using TypeScript on our backend, I was incredibly frustrated. It felt like I needed to constantly work around the type system, and it felt like the types weren't providing any value at all. And, they weren't providing any value to me: I had fundamental misconceptions about how TypeScript worked, and my attempts to work around the Type system by using lots of `any` and `as` created some truly abominable TypeScript code.]]></description><link>https://engineering.classdojo.com/2021/08/10/3-confusing-things-about-typescript</link><guid isPermaLink="true">https://engineering.classdojo.com/2021/08/10/3-confusing-things-about-typescript</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Tue, 10 Aug 2021 00:00:00 GMT</pubDate></item><item><title><![CDATA[Manipulating Text & Files solves problems]]></title><description><![CDATA[A decent number of programming tasks boil down to manipulating text and files:]]></description><link>https://engineering.classdojo.com/2021/08/02/manipulating-text-and-files-solves-problems</link><guid isPermaLink="true">https://engineering.classdojo.com/2021/08/02/manipulating-text-and-files-solves-problems</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Mon, 02 Aug 2021 00:00:00 GMT</pubDate></item><item><title><![CDATA[HyperLogLog-orrhea]]></title><description><![CDATA[Say you had a few petabytes of user ids lying around somewhere, and someone told you that they were going to use a few kb of memory to estimate the "cardinality", or the number of distinct ids, of those petabytes of ids with decently high accuracy. It'd feel like magic! The HyperLogLog algorithm makes that magic cardinality estimation happen through the Power of Random Numbers, and some clever use of data structures.]]></description><link>https://engineering.classdojo.com/2021/07/23/hyperloglog-orrhea</link><guid isPermaLink="true">https://engineering.classdojo.com/2021/07/23/hyperloglog-orrhea</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Fri, 23 Jul 2021 00:00:00 GMT</pubDate></item><item><title><![CDATA[Creating an Actionable Logging Taxonomy]]></title><description><![CDATA[The standard syslog-based way to handle logs is to divide logs into categories like `FATAL`, `ERROR`, `WARN`,`INFO`, and `DEBUG` and use those categories to adjust which logs you see. Having a standard logging taxonomy is incredibly useful when you're dealing with logs for many different systems, but for our backend web-servers, we didn't find these syslog-based divisions to be actionable or easily searchable. We instead created our own categories that map to how we want to handle our logs. We divided our logs into `server-errors`, `client-errors`, and `sampled-investigation-logs`, and added tags to route logs in these categories to the team & person who's best suited to handle them.]]></description><link>https://engineering.classdojo.com/2021/07/20/actionable-logging-taxonomy</link><guid isPermaLink="true">https://engineering.classdojo.com/2021/07/20/actionable-logging-taxonomy</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Tue, 20 Jul 2021 00:00:00 GMT</pubDate></item><item><title><![CDATA[Graceful web-server shutdowns behind HAProxy]]></title><description><![CDATA[Fully graceful incremental deploys are hard. Any number of events can deliver brief spates of 5xx errors to your clients, and getting all of the pieces right isn't trivial. And, if something isn't quite right, it can be hard to detect problems from a few seconds of downtime on a server over the course of a deploy.]]></description><link>https://engineering.classdojo.com/2021/07/13/haproxy-graceful-server-shutdowns</link><guid isPermaLink="true">https://engineering.classdojo.com/2021/07/13/haproxy-graceful-server-shutdowns</guid><dc:creator><![CDATA[Will Keleher]]></dc:creator><pubDate>Tue, 13 Jul 2021 00:00:00 GMT</pubDate></item><item><title><![CDATA[Service Discovery With Consul at ClassDojo]]></title><description><![CDATA[ClassDojo has a fleet of services that work together to serve user requests as well as complete other data operation tasks. Most of these services are set to auto-scale to better utilize our resources. Part of doing this involves synchronizing a lot of metadata like tags and service ports to keep our configurations up to date. We started thinking about how we could improve distributed runtime changes to our infrastructure-wide config and service discovery as servers join and leave the fleet.]]></description><link>https://engineering.classdojo.com/2018/05/10/service-discovery-with-consul-at-classdojo</link><guid isPermaLink="true">https://engineering.classdojo.com/2018/05/10/service-discovery-with-consul-at-classdojo</guid><category><![CDATA[Engineering]]></category><category><![CDATA[Performance]]></category><category><![CDATA[Service-descovery]]></category><category><![CDATA[Consul]]></category><dc:creator><![CDATA[April Ablon]]></dc:creator><pubDate>Thu, 10 May 2018 00:00:00 GMT</pubDate></item><item><title><![CDATA[Powering Class Story With Texture]]></title><description><![CDATA[
![GATSBY_EMPTY_ALT](https://dojo-directus.s3.amazonaws.com/uploads/d2e306a1-faaf-4b13-a597-b4a0cae5e599.png)

One of the most effective products we've launched is Story. As a part of our mission to help teachers, parents and students create incredible classrooms, we released Story as a way to refocus the conversation around the incredible "ah ha!" moments students experience. ClassDojo launched Class Story in September 2015, since then we've added Student Story, Story for Parents, School Story, and just recently, Story for student accounts. We decided to use [Texture](http://texturegroup.org/) to power Story so we can continue to deliver unique experiences to classrooms worldwide.

<!-- more -->

We initially used [UIKit](https://developer.apple.com/documentation/uikit) to power our first version of story: Class Story. The spec only allowed photo, text, or photo and text posts for teachers So we used [UITableView](https://developer.apple.com/documentation/uikit/uitableview) and [UITableViewCell](https://developer.apple.com/documentation/uikit/uitableviewcell) for the views. We use [Core Data](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/index.html) to store all of the data, so we can leverage convenient UIKit classes like [NSFetchedResultsController](https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller) to make the data-binding easier. However, as the product grew, we started running into common issues with our UIKit-based solution. Notably:

- Out of bounds exceptions on [NSFetchedResultsControllerDelegate](https://developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate) calls
- Scroll slowdown on older devices
- [NSIndexPath](https://developer.apple.com/documentation/foundation/nsindexpath) wrangling to handle cells above and below Core-Data backed cells
- Height calculation issues on cells
- [Auto Layout](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/index.html) issues
- Preloading older data as the user scrolled to the bottom of a feed (i.e. infinite scrolling)

We needed to find a better set of solutions to tackle each issue. But it wasn't as if we were the first team ever to try and figure out how to develop a feed - there are several open-source solutions for handling this use-case. Our game plan was to examine such solutions and see if they were better than refactoring and staying with UIKit. We looked at a few of these libraries ([ComponentKit](http://componentkit.org/), [IGListKit](https://instagram.github.io/IGListKit/), and [Texture - formerly AsyncDisplayKit](http://texturegroup.org/)) and built a few test apps to see what kinds of costs/benefits each provided. We ultimately ended up settling on Texture because of its high-performance scrolling, ability to handle complex user-interactions, large contributor base and intelligent preloading for infinite scroll.  

Texture gave a few wins right out of the box: height calculation, autolayout constraint issues, and infinite scrolling were effectively solved for us. Texture implements a [flexbox-styled layout system](http://texturegroup.org/docs/automatic-layout-examples-2.html) that infers height from a layout specification object each view can choose to implement. Layout and render calculations are all performed on background threads, which allows the main thread to remain available for user-interactions and animations. A convenient delegate method, [`-tableView:(ASTableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context`](http://texturegroup.org/appledoc/Protocols/ASTableDelegate.html#//api/name/tableNode:willBeginBatchFetchWithContext:) allows us to detect when the user is going to scroll to the bottom of the screen and fire the network requests necessary to provide a smooth, infinite-scroll, experience.

Handling NSIndexPath calculations required a bit of additional wrangling. Quite a few of our feeds include auxiliary cells at the top or bottom of the feed. These cells usually have some kind of call to action inviting users down some deeper user flow we are trying to encourage. Implementing these cells in UIKit and Core Data caused us to implement some rather annoying index calculations. Most of these calculations arose because of constraints around NSFetchedResultsController. The fetched results controller is a convenient class that allows handling of predicates, Core Data update handlers, and data fetching. The problem is that the fetched results controller always assumes that its indices start from row 0. Adding additional cells above or below require calculations on detecting whether or not a given index is for an auxiliary cell and, if not, requires a transformation on the index so that NSFetchedResultsController knows how to handle it.

```objectivec
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  if ([self isIndexPathForFirstHeaderCell:indexPath]) {
      // calculate cell here
      return cell;
  } 

   NSIndexPath *adjustedIndexPath = [self adjustedIndexPathForFetchedResultsController:indexPath];
   id data = [self.fetchedResultsController objectAtIndex:adjustedIndexPath];
   // calculate cell here
   return cell;
}
```
There is one main problem with this approach: if the order of the data models backing the auxiliary cells is modified, we have to remember to recalculate the indicies. If those calculations are incorrect, the wrong data would show up for a given cell (at best) or the app would crash (at worst).

In order to fix this issue, we opted to check for the class of the backing data and having the delegate methods map it to the appropriate logic.

```objectivec
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

  id backingData = [self dataForIndexPath:indexPath];
  class dataClass = [backingData class];

  if (dataClass == [AuxiliaryData class]) {
      // Configure and return AuxiliaryCell
  } else if (dataClass == [ManagedObjectCustomClass class]) {
    // Configure and return managed object table view cell
  }
}
```

This approach gives us two advantages. First, it solves the issue of index calculation not matching up with the backing data object. Checking the class allows the data binding to be index agnostic; instead of indexes determining views, the data determines the appropriate view. This became especially important in crushing bugs regarding interactivity: because the cell indexes directly match the backing data, there isn’t a chance that tapping a cell would trigger the logic for the cell above, or the cell below. Secondly, this approach eliminated a common bug where a newly added or deleted auxiliary cell caused Core Data backed cells to be visible or not. With the index calculation, it was possible that a cell previously held by a Core Data object would now have an NSIndexPath associated with an auxiliary cell (or vice-versa). By checking the class of the data, we don’t have to worry about the timing in which the indicies are updated and whether or not it matches up in time with Core Data automatic updates.

Texture also comes with a rather nifty preloading system. It provides a convenient method that notifies the system when the user is n screen-lengths away from the bottom of the screen (n is defaulted to 2). The method gives a simple context object which needs to be notified when data is being fetched and when the fetching is completed.

```objectivec
- (void)tableView:(ASTableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context {
    
    [context beginBatchFetching];

    // do fetch work

    [context completeBatchFetching:YES];
}
```

We decided to use the NSFetchedResultsControllerDelegate along with Texture’s [ASTableNode](http://texturegroup.org/appledoc/Classes/ASTableNode.html) batchUpdate system to have this batch fetch system automatically update the upcoming cells offscreen. In order to do this properly, the context can’t be given the `completeBatchFetching:` signal until both the data fetch and the animation update has been completed. To do this, we use a single property to keep track of the current state.

```objectivec
typedef NS_OPTIONS(NSUInteger, CDBFeedItemSyncMoreState) {
    CDBFeedItemSyncMoreStateNone = 0,
    CDBFeedItemSyncMoreStateRequestCompleted = 1 << 0,
    CDBFeedItemSyncMoreStateViewUpdateCompleted = 1 << 1,
};

- (void)setSyncMoreState:(CDBFeedItemSyncMoreState)syncMoreState {
    
    BOOL requestFinished = syncMoreState & CDBFeedItemSyncMoreStateRequestCompleted;
    BOOL viewsUpdated = syncMoreState & CDBFeedItemSyncMoreStateViewUpdateCompleted;
    
    if (requestFinished && viewsUpdated) {
        
        if (self.batchContext) {
            [self.batchContext completeBatchFetching:YES];
            self.batchContext = nil;
        }
        _syncMoreState = CDBFeedItemSyncMoreStateNone;
    } else {
        
        _syncMoreState = syncMoreState;
    }
}
```

When the data fetch request completes, it simply sets the syncMoreState property to

```objectivec
self.syncMoreState = self.syncMoreState|CDBFeedItemSyncMoreStateRequestCompleted
```

When the view update completes, it updates the syncMoreState property as well

```objectivec
- (void)updateForChanges:(CDUFetchedResultsUpdateModel *)updateModel {
        
    __weak typeof(self) weakSelf = self;
    [self.tableNode handleFetchedResultsUpdates:updateModel completion:^(BOOL finished) {
        
        __strong typeof(weakSelf) strongSelf = weakSelf;
        
        if (weakSelf.batchContext) {
            
            weakSelf.syncMoreState = weakSelf.syncMoreState|CDBFeedItemSyncMoreStateViewUpdateCompleted;
        }        
    }];
}
```

This approach allowed us to connect the existing functionality in UIKit with the performance and syntactic-sugar that Texture provides. 

Here is a side-by-side comparison on an iPad Gen 3 - iOS 8:

<video class='center-image' width="500" controls>
  <source src="https://static.classdojo.com/img/engineering_blog/uikit-texture-side-by-side.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

However, Texture did come without its own set of challenges. In particular, Texture’s reliance on background dispatch pools created interesting threading problems with our Core Data stack. Texture tries to call init and view loading methods on background threads where possible, but often those were the same places where Core Data objects were being referenced. Because our NSFetchedResultsControllers always run on the main thread, these background dispatch pools often caused concurrency warnings when a Texture view attempted to use an [NSManagedObject](https://developer.apple.com/documentation/coredata/nsmanagedobject). 

To address this, we strictly enforce read-only operations on the main queue [NSManagedObjectContext](https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext). By limiting the main queue to only read-only operations, we are able to get the latest data from the NSFetchedResultsController while preventing any update/insert/delete operations that may cause instability in other parts of the app. Overall, this is the least satisfactory aspect of Texture and is an ongoing area of exploration for us.

Like any library, Texture provides a series of tradeoffs. It allows us to leverage existing UIKit technology and [GCD](https://developer.apple.com/documentation/dispatch), but exposed us to an entirely new set of potential concurrency issues. However, we’ve found Texture provides a smoother scrolling experience, better performance on older devices, and allows us to have a higher velocity in developing new functionality. Overall, if your application includes an Instagram/Pinterest style feed, we encourage you try out Texture for yourself!]]></description><link>https://engineering.classdojo.com/2017/07/13/texture</link><guid isPermaLink="true">https://engineering.classdojo.com/2017/07/13/texture</guid><category><![CDATA[Programming]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Mobile]]></category><category><![CDATA[Story]]></category><dc:creator><![CDATA[Paul Kim]]></dc:creator><pubDate>Thu, 13 Jul 2017 00:00:00 GMT</pubDate></item><item><title><![CDATA[Lifecycle Emails at ClassDojo (Part I)]]></title><description><![CDATA[We want ClassDojo to be a delightful product for teachers and parents everywhere, and we've found that email is an]]></description><link>https://engineering.classdojo.com/2017/06/13/lifecycle-emails-at-classdojo-i</link><guid isPermaLink="true">https://engineering.classdojo.com/2017/06/13/lifecycle-emails-at-classdojo-i</guid><category><![CDATA[Programming]]></category><category><![CDATA[Redshift]]></category><category><![CDATA[Lifecycle]]></category><category><![CDATA[Email campaign]]></category><dc:creator><![CDATA[Jet Zhou]]></dc:creator><pubDate>Tue, 13 Jun 2017 00:00:00 GMT</pubDate></item><item><title><![CDATA[Running 4558 Tests in 1m 55sec (or Saving 50 Hours/week)]]></title><description><![CDATA[About four months ago, our API tests took about 12-15 minutes to run on a decently powered linux machine. Test runtimes on Macs were slightly slower even with comparable hardware. Today it takes us 1 minute and 55 seconds to run the entire test suite including setup & teardown saving us `~50 hours` of development time every week.]]></description><link>https://engineering.classdojo.com/2017/05/22/running-4558-tests-in-1m-55sec</link><guid isPermaLink="true">https://engineering.classdojo.com/2017/05/22/running-4558-tests-in-1m-55sec</guid><category><![CDATA[Continuous-integration]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Docker-compose]]></category><category><![CDATA[Engineering]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Performance]]></category><dc:creator><![CDATA[Urjit Singh Bhatia]]></dc:creator><pubDate>Mon, 22 May 2017 00:00:00 GMT</pubDate></item><item><title><![CDATA[A/B Testing From Scratch at ClassDojo]]></title><description><![CDATA[At ClassDojo, we're working on building amazing classroom communities at scale. To succeed in this mission, we have to run lots of product experiments, and just as importantly, accurately measure their results. In this post, I'll outline how we built our A/B testing system, and how we used it to run one of those experiments.]]></description><link>https://engineering.classdojo.com/2017/04/01/ab-testing-from-scratch</link><guid isPermaLink="true">https://engineering.classdojo.com/2017/04/01/ab-testing-from-scratch</guid><category><![CDATA[Ab-testing]]></category><category><![CDATA[Statistics]]></category><category><![CDATA[Programming]]></category><dc:creator><![CDATA[Lexi Ross]]></dc:creator><pubDate>Sat, 01 Apr 2017 00:00:00 GMT</pubDate></item><item><title><![CDATA[Speeding Up Android Builds: Tips & Tricks]]></title><description><![CDATA[We build our software with a focus on Continuous Integration and quick deployment lifecycles. Naturally, speed is a critical component, allowing faster iterations and quick feedback.]]></description><link>https://engineering.classdojo.com/2017/01/24/speeding-up-android-builds-tips-and-tricks</link><guid isPermaLink="true">https://engineering.classdojo.com/2017/01/24/speeding-up-android-builds-tips-and-tricks</guid><category><![CDATA[Programming]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Speed]]></category><category><![CDATA[Jenkins]]></category><category><![CDATA[Build]]></category><category><![CDATA[Gradle]]></category><category><![CDATA[CI]]></category><category><![CDATA[Android]]></category><category><![CDATA[Artifactory]]></category><dc:creator><![CDATA[Urjit Singh Bhatia]]></dc:creator><pubDate>Tue, 24 Jan 2017 00:00:00 GMT</pubDate></item><item><title><![CDATA[Integration Testing React and Redux With Mocha and Enzyme]]></title><description><![CDATA[At ClassDojo, our frontend web apps are large single-page applications running in React and Redux. Millions of teachers, parents, and students use these apps every day, and we want to ensure a quality experience for them. However, effectively testing frontend apps is hard.]]></description><link>https://engineering.classdojo.com/2017/01/12/integration-testing-react-redux</link><guid isPermaLink="true">https://engineering.classdojo.com/2017/01/12/integration-testing-react-redux</guid><category><![CDATA[Redux]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Programming]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Peter Hayes]]></dc:creator><pubDate>Thu, 12 Jan 2017 00:00:00 GMT</pubDate></item><item><title><![CDATA[Catching React Errors With ReactTryCatchBatchingStrategy]]></title><description><![CDATA[At ClassDojo, we’ve been working on getting visibility into bugs that get deployed to our production React webapp. Catching errors in our data layer is pretty straightforward, but the view layer presents more of a challenge.]]></description><link>https://engineering.classdojo.com/2016/12/10/catching-react-errors</link><guid isPermaLink="true">https://engineering.classdojo.com/2016/12/10/catching-react-errors</guid><category><![CDATA[Errors]]></category><category><![CDATA[Programming]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Byron Wong]]></dc:creator><pubDate>Sat, 10 Dec 2016 00:00:00 GMT</pubDate></item><item><title><![CDATA[Creating Photo Collages With Node-canvas]]></title><description><![CDATA[Class Story is one of our core product features at ClassDojo.  It allows teachers to post photos and videos that parents can view, like, and comment on, and teachers around the world use it to share cute, funny, or exciting moments from their classrooms.  Since we’re approaching the end of the school year, we wanted to provide teachers with a something memorable and engaging to send home.  But with a ton of new projects we’d like to ship before back-to-school in August, we also wanted something we could build quickly.]]></description><link>https://engineering.classdojo.com/2016/05/26/collages-with-redshift-highland-and-canvas</link><guid isPermaLink="true">https://engineering.classdojo.com/2016/05/26/collages-with-redshift-highland-and-canvas</guid><category><![CDATA[Programming]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Highland]]></category><category><![CDATA[Canvas]]></category><category><![CDATA[Redshift]]></category><category><![CDATA[Streams]]></category><dc:creator><![CDATA[Peter Hayes]]></dc:creator><pubDate>Thu, 26 May 2016 00:00:00 GMT</pubDate></item><item><title><![CDATA[Better Rate Limiting With Redis Sorted Sets]]></title><description><![CDATA[At ClassDojo, we've recently been building out our push notification infrastructure.  Our plans required a rate limiter that met the following criteria:  ]]></description><link>https://engineering.classdojo.com/2015/02/06/rolling-rate-limiter</link><guid isPermaLink="true">https://engineering.classdojo.com/2015/02/06/rolling-rate-limiter</guid><category><![CDATA[Programming]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Redis]]></category><dc:creator><![CDATA[Peter Hayes]]></dc:creator><pubDate>Fri, 06 Feb 2015 00:00:00 GMT</pubDate></item><item><title><![CDATA[Continuous Testing at ClassDojo]]></title><description><![CDATA[We have thousands of tests and regularly deploy to production multiple times per day.  This article is about all the crazy things we do to make that possible.]]></description><link>https://engineering.classdojo.com/2015/01/20/continuous-testing-at-classdojo</link><guid isPermaLink="true">https://engineering.classdojo.com/2015/01/20/continuous-testing-at-classdojo</guid><category><![CDATA[Quality]]></category><category><![CDATA[Programming]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Gregg Caines]]></dc:creator><pubDate>Tue, 20 Jan 2015 00:00:00 GMT</pubDate></item><item><title><![CDATA[Fs-Tail]]></title><description><![CDATA[[Fs-Tail](https://github.com/classdojo/fs-tail) is a simple library we wrote that provides `tail -f`-like functionality using Node’s v0.10+ streams and fs modules. Install via npm:]]></description><link>https://engineering.classdojo.com/2014/12/18/fs-tail</link><guid isPermaLink="true">https://engineering.classdojo.com/2014/12/18/fs-tail</guid><category><![CDATA[Data]]></category><category><![CDATA[Programming]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Chris Olivares]]></dc:creator><pubDate>Thu, 18 Dec 2014 00:00:00 GMT</pubDate></item><item><title><![CDATA[Production-Quality Node.js (Part III): Preventing Defects]]></title><description><![CDATA[This is the last part of my 3-part series on production-quality Node.js web applications.  I've decided to leave error-prevention to the end, because while it's super-important, it's often the only strategy developers employ to ensure customers have the most defect-free experience as possible.  It's definitely worth checking out parts [I](http://caines.ca/blog/2014/06/01/production-quality-node-dot-js-web-apps-part-i/) and [II](http://caines.ca/blog/2014/06/02/production-quality-node-dot-js-web-apps-part-ii/) if you're curious about other strategies and really interested in getting significant results.]]></description><link>https://engineering.classdojo.com/2014/06/23/production-quality-node-dot-js-part-iii</link><guid isPermaLink="true">https://engineering.classdojo.com/2014/06/23/production-quality-node-dot-js-part-iii</guid><category><![CDATA[Quality]]></category><category><![CDATA[Programming]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Gregg Caines]]></dc:creator><pubDate>Mon, 23 Jun 2014 00:00:00 GMT</pubDate></item><item><title><![CDATA[Production-Quality Node.js (Part II): Detecting Defects]]></title><description><![CDATA[This is the second part of a three part series on making node.js web apps that are (what I would consider to be) production quality.  The [first part](/blog/2014/06/01/production-quality-node-dot-js-web-apps-part-i/) really just covered the basics, so if you're missing some aspect covered in the basics, you're likely going to have some issues with the approaches discussed here.]]></description><link>https://engineering.classdojo.com/2014/06/03/production-quality-node-dot-js-part-ii</link><guid isPermaLink="true">https://engineering.classdojo.com/2014/06/03/production-quality-node-dot-js-part-ii</guid><category><![CDATA[Quality]]></category><category><![CDATA[Programming]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Metrics]]></category><dc:creator><![CDATA[Gregg Caines]]></dc:creator><pubDate>Tue, 03 Jun 2014 00:00:00 GMT</pubDate></item><item><title><![CDATA[Production-Quality Node.js (Part I): The Basics]]></title><description><![CDATA[I've been working on production-quality Node.js web applications for a couple of years now, and I thought it'd be worth writing down some of the more interesting tricks that I've learned along the way.]]></description><link>https://engineering.classdojo.com/2014/06/01/production-quality-node-dot-js-part-i</link><guid isPermaLink="true">https://engineering.classdojo.com/2014/06/01/production-quality-node-dot-js-part-i</guid><category><![CDATA[Quality]]></category><category><![CDATA[Programming]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Gregg Caines]]></dc:creator><pubDate>Sun, 01 Jun 2014 00:00:00 GMT</pubDate></item></channel></rss>