Using JIT Learning to Build an App During Launch School’s Capstone Prep

Jason Wang
Launch School
Published in
14 min readDec 30, 2022

--

For another perspective on the Core-to-Capstone transition, please also read my teammate Ethan’s article on learning styles.

Just-In-Time (JIT) learning means acquiring skills when they’re needed, instead of in advance. When we’re in a time crunch and need to learn new tools quickly, having this ability is indispensable. When we change projects or roles, it helps us get up to speed with new technologies and codebases. JIT learning is unlocked after developing strong fundamentals and study habits. Those are gained through Core’s Mastery-Based Learning (MBL) approach.

Capstone Prep is the study period between Core and Capstone. It’s a good training ground for practicing JIT learning because we cover several new topics in a short span. Since there’s not enough time to master everything, we have to decide when we’ve studied enough and when to move on. It was difficult transitioning from from MBL to JIT learning because I had to accept that I wouldn’t be able to learn every detail.

Learning as a Team

In Core, MBL means taking as long as we need to achieve mastery and pass assessments. There are no student classes that advance together, so it may be lonely watching others progress faster or slower. In Capstone Prep teams are assigned from the start, so it’s easier to sync study schedules. Discussing material with peers has been tremendously helpful when trying to JIT learn at a fast pace. Meeting regularly with my team has patched countless holes in my newly-forming mental models. I’m grateful to have such motivated and competent teammates: Ethan and Ryan. Thank you both for all the emotional and technical support, from sharing career aspirations to reviewing my writing and code.

Applying JIT Learning to a Project

When studying related subjects, I find it useful to build a project that integrates everything I’m learning. Working toward a tangible end product keeps me focused and engaged. For every feature I want to add, I end up JIT learning a few tools and figuring out how to glue them together.

The App: “In Season”

When we go grocery shopping, my partner and I always consider “What’s in season right now?” because those foods are often cheaper and more flavorful. In Season is a CRUD app for tracking what fruits and vegetables are in season each month. The initial data is for Southern California, and with an account you can edit and add foods. The basket feature is a shopping list.

https://in-season-now.com (inactive as of 11/26/23; AWS Free Tier ended)

https://github.com/jasonherngwang/in-season-frontend

Data sources: CA Grown, Seasonal Food Guide, CA Harvest Calendar

Timeboxing the Project

Timeboxing means setting a time limit for an activity. Based on that limit we determine a realistic scope of work. MBL doesn’t have due dates, but situations requiring JIT learning usually do. At the start of the project, I asked myself:

How long is the timebox? I will try to finish in 2–3 weeks, taking a few days to build each major component.

Where are your knowledge gaps? I know JavaScript and need to learn TypeScript. I’m familiar with the MERN stack, containers, and server setup, but have never integrated them together. I know some theory about cloud services but have no practical experience.

Learning Strategies

During MBL we’re encouraged to try different study strategies and discover what works for us. When JIT learning, we rely on those ingrained strategies to quickly grasp new concepts. For me, circular learning and frequent experimentation work best. I make a fast first pass with some notes, and on subsequent passes I improve those notes. I also write many code examples to remind me how a function or library behaves. My overall approach is similar to the guidance we receive in JS230:

  1. Perform a high-level review to determine if a tool is suitable.
  2. Learn theory by reading documentation.
  3. Gain practice through tutorials and small projects.
  4. Make additional passes through the material.

This post describes my JIT learning experience with parts of the project.

Building the UI with React

React is a JavaScript library for building user interfaces (UI). It’s declarative, which means that we describe the final UI state, and React updates the DOM to get there. In contrast, jQuery is an imperative DOM manipulation library. We use it to write step-by-step instructions on how to reach the final state.

You can build a React app without understanding JavaScript. That’s how I got started with web development. However, I began to struggle as the app became more complex, and my lack of understanding only snowballed when I tried to push through it. Eventually I realized that I should have learned fundamentals before frameworks, which led me to Launch School.

A Foundation of Fundamentals

The skills and knowledge we gain at Launch School can be used to build functional apps, but also do much more. We’re here to gain deep understanding so we can troubleshoot problems, make design decisions, and discuss tradeoffs. Eventually we may even architect large-scale distributed systems. And the first step of that journey is developing solid fundamentals.

Fundamentals enable JIT learning because we rely on basic concepts to understand complicated ones. Here are examples of how Core material helped me understand React:

  • Fluency with JavaScript (JS210) helped me move through the React docs without getting bogged down by syntax and promises. That freed my mind up to focus on new concepts like state, hooks, and JSX.
  • Familiarity with the DOM (JS230) helped me comprehend how React compares the current and virtual DOM trees to identify component updates.
  • Understanding closures (JS225) helped me realize that event handlers close over variables from the latest render, so we need to wait until the next render to see updated values.
  • Experience with jQuery (JS230) helped me make the mental shift from imperative to declarative. Knowing the “old” way allowed me to appreciate the “new” way.

Thinking in React

Part of my learning approach is constant experimentation. Modern tooling makes this easy; CRA or Vite can scaffold a project in just a few seconds. I made at least 10 tiny apps to try things like hooks, TypeScript, Redux, and React Router. When it was time to write the UI, it was a matter of stitching together everything I had learned.

“Thinking in React” means breaking the UI into components, analyzing data flows, and adding interactivity with state. Having modular components makes it easier to manage and reuse code. It separates concerns so each component is only responsible for a specific purpose. There’s also a performance benefit: since React updates a component and its descendants when state changes, we can avoid unnecessary updates by carefully organizing the component hierarchy.

Before I started coding, I created the following mockup to identify components. This helped me visualize the UI as a composition of individual building blocks. Next, I coded them as static (non-interactive) React components. Finally, I connected them all by writing logic for navigation and data management.

Adding Interactivity with State

State is a component’s memory. It’s data that changes in response to user input and events, and we can use it to build reusable interactive apps. React is responsible for synchronizing the UI with state. When I add food to my basket or type in a food description, the component remembers and displays the latest information.

State can be managed in various ways. I used a mix of global (centralized) state and local component state. The following diagram shows how I use global state to initially populate form fields, use local state to capture user edits, and send those edits to the backend for storage in the database.

Reflections

At the end of a timebox, it’s useful to reflect upon what we’ve done well and could do better. I feel like I’ve barely scratched the surface of React and could improve my code structure and patterns. For example, some of my components try to do too much: they manage state, side effects, and visual elements all at once. Extracting code into smaller components would help. As for performance I could pre-render HTML for static pages like Plans, instead of rendering them client-side. Despite having many areas to work on, I’m still happy I was able to build a functional app.

Now that I’ve gained context of React and its place in the frontend, many new learning paths have been unlocked. A glance at roadmap.sh and patterns.dev shows that I could look more into React hooks, rendering patterns, CSS frameworks, and more. Whichever I choose, JIT learning will the key to gaining that knowledge quickly.

Thinking About Data: From SQL to NoSQL

LS180 covers relational databases. Tables are connected using keys, and data is normalized to reduce duplication. Schemas are defined and enforced. We write SQL JOINs to combine data from multiple tables. An Entity Relationship Diagram (ERD) for the app looks like the following, where a user has many foods and a basket to keep track of purchases.

In Capstone Prep we meet non-relational (NoSQL) document databases, which require a different mental model. They have a more flexible schema which allows us to store less-structured data as nested documents and arrays. The data structure looks like JSON, so it’s easier for us to understand. We may denormalize data, so multiple documents can contain duplicates of the same data. This enables better performance by reducing lookups.

Creating New Mental Models

When choosing a database we analyze the data format, access patterns, relationships, and other factors. Since I lacked extensive database experience I struggled a lot with this. Even though I was using a non-relational database, I instinctively leaned toward the relational approach I was comfortable with. However, part of JIT learning is researching the domain to evolve and create mental models. By the end of the project I gained a better understanding of the differences between relational and non-relational.

I started by modeling the data as a User with an array of Food IDs (One to Many relationship); this is the Document Reference pattern. It meshed well with my existing relational mental model of primary and foreign keys, keeping User and Food entities separate. After working with the data some more and adding an Edit Food feature, I realized that this data model was inefficient.

Each User has their own unique customizable set of Foods, so no two Users should reference the same Food. Therefore it made more sense to bundle the Food data inside the User data structure. After reading through the MongoDB docs I refactored to the Embedded Document pattern. A single query could return all data in a self-contained User document, so no additional queries would be needed to retrieve Foods by ID. This made reads faster, but during writes if the Food was also in the User’s Basket, those fields had to be updated as well.

Reflections

For databases and other software engineering domains, the knowledge pool is so large that we could spend an entire career in it. This is a blessing for those who love to learn, but a curse for those who aren’t satisfied until they’ve mastered every detail. I am both of these, and many Launch School students probably feel the same.

So is it possible to JIT learn something as complex as databases? I suppose it depends on your goals. If you’re fascinated by database internals, then you might enjoy a longer-term approach like Bradfield CSI. If you’re in my situation and need to get an app running, you can start with some high-level theory and practice with the query language and libraries.

I decided to move on after reaching a working MongoDB implementation that demonstrated NoSQL benefits, specifically the concept of embedded documents reducing reads. The end result was adequate, yet I still have many questions. Availability vs consistency? Replication and sharding? Resolving concurrent writes? Those are discussions I’m looking forward to in Capstone.

Deploying to the Cloud

During Core we code on our own computers and don’t have many users. Once the output is correct, we can complete the assignment. In reality, as working engineers we ship software for others to use, and the environment outside our laptops is complex and unpredictable. Apart from writing code that runs, we’ll also need to consider its reliability, scalability, and performance. Deploying on cloud infrastructure can help meet some of those requirements.

Before Capstone Prep I had a vague understanding of what the cloud was. So a primary goal of my JIT learning was to review the use cases and benefits of the cloud. For the project, I wanted to configure a virtual server to host my app and run some containers. It didn’t have to scale to a million users or tolerate data center failure; it just needed to be online. My plan of study:

  • Review LS170 because networking knowledge is essential for working with the cloud.
  • Gain conceptual understanding from videos on Amazon Web Services (AWS) and Docker.
  • Gain practical understanding through tutorials on AWS, AWS SDK, and Docker.
  • Get the app working locally and then move it to an AWS EC2 instance.

What is the Cloud?

The cloud is a network of Internet-connected servers used for storing and managing data. Cloud providers like AWS and DigitalOcean operate these servers in their data centers, and we purchase cloud services from them. We rent their computers to run apps and store data, and we can access these resources on-demand.

The providers manage a large amount of physical infrastructure and can offer lower prices compared to if we set up our own servers. They perform many optimizations like serving multiple customers from a single machine. With so much computing power, they can instantly provide us as much or as little resources as we need. This dynamic usage model is attractive because we only pay for what we use.

Cloud computing is the model of delivering computing services over the Internet. Services operate on a stack of infrastructure layers, shown in the table below. The term “X-as-a-Service” refers to the separation of responsibilities between the provider and customer. For example, using a Platform-as-a-Service (PaaS) means we can focus on writing our app code and let the provider handle everything else.

If you code in Cloud9, you’re using the AWS EC2 Infrastructure-as-a-Service (IaaS). If you’ve completed RB185, you may have deployed an app using Heroku’s PaaS and Postgres Database-as-a-Service. And if you’re in Slack or Gather, you’re using Software-as-a-Service (SaaS).

What is a Container?

My code may not run on your computer because our environments are different. We might have different operating systems (OS), packages, or configuration settings. But what if I bundled the code and its dependencies into a self-contained package that runs the same anywhere? That’s called a container image. If you and I both install Docker software, that container will run exactly the same on our computers. Being able to run code in a predictable and consistent way is a useful tool for deploying apps to different places and collaborating with a distributed team.

The App Architecture

The app is a set of processes running on Linux. The OS is installed on a virtual machine (VM). A VM is software that behaves like a regular computer, but it doesn’t necessarily have its own dedicated hardware. Multiple VMs can share hardware on a cloud provider’s physical server, so my app’s VM may have neighbor VMs it knows nothing about. The server hosting my app is in an AWS data center on the U.S. East Coast.

The app has three containers, which must all be running for the app to work:

  1. Web Tier: For serving static assets. When you visit the website, the server sends back index.html. Your browser makes additional requests for the React app (index.js) and CSS (index.css). It also requests resources from other locations, e.g. fonts from Google and food images from AWS CloudFront.
  2. App Tier: When you click on things, the React app makes requests to the /api endpoints for data. The Web Tier forwards (reverse proxies) these requests to the App Tier where the business logic lives. Inside the Node.js runtime environment an Express app handles these requests. It communicates with the database to perform tasks such as logging in or adding foods to the basket. Data is returned to your browser so React can display any updates.
  3. Data Tier: An instance of MongoDB manages the app data, which is stored on the VM.

I used Docker Compose to define and coordinate the containers. There are separate frontend and backend networks so Web can talk to App, and App can talk to Data. Fundamentals from LS170 helped me configure the ports and understand how Docker networks behave. A convenient feature is service discovery, which lets us address containers by name.

Note: This is an experimental project with inefficiencies. There are redundant reverse proxies on the EC2 instance and the Web Tier. I could move the static files to the instance or the App Tier. It would also be a good idea to use a managed database service.

File Storage and Distribution

The app loads 70 food images, which can be slow. To improve user experience, I store the images in AWS Simple Storage Service (S3) and distribute them with the AWS CloudFront content delivery network (CDN). AWS has edge locations (servers) around the world, which function as caches (temporary holding locations). When you request an image, AWS will first check a nearby edge location to see if it can return the image quickly. If it’s not there, the image is fetched from an upstream source and stored so future requests are faster.

Practicing With the AWS SDK

AWS provides Software Development Kit (SDK) libraries so we can use JavaScript to interact with resources like S3. There are many tutorials and workshops to get acquainted with the SDK.

The app lets users upload images when editing or adding foods. The following diagram shows how it works. In the Express app I created a route which uses my AWS credentials to PUT the image to S3. If successful it returns the image URL, which is used to display the new image in the UI. It took some experimentation with the SDK to figure out what parameters to use when sending files to S3. In the future I will look into using presigned URLs.

Reflections

Working with cloud services has expanded my view on how apps can be deployed. I still have so much to learn and can already see several flaws with my configuration. The components are tightly coupled and can’t be managed independently. The app only runs in 1 zone and lacks resilience. There’s no scaling, so with high traffic it won’t be able to keep up. And my database has no backups.

The bright side is that with this new context I can continue exploring better models and architectures. For example:

  • Moving each tier to its own instance
  • Adding a load balancer and an auto scaling group
  • Going serverless with an API Gateway and Lambda functions
  • Making sense of dynamic container orchestration services

Final Thoughts

Core is a long and difficult journey, punctuated by small victories. There are moments of joy when we pass an assessment or finally understand a confusing concept, but most of our time is spent struggling with fundamentals until they become second nature. Even if we’re onboard with Launch School’s long-term approach, it can be easy to get discouraged when the end seems so far away. So is all this effort worth it? And what exactly is the end result of completing Core?

For me, the reward for persevering and studying diligently was a genuine transformation in ability and attitude. I gained this JIT learning superpower that I can use to pick up anything quickly. During Capstone Prep I got a glimpse of how useful JIT learning is, and I’ll be developing this skill further in Capstone. Along with it came a growth mindset which gives me confidence that one day I can excel as a software engineer. If you’re in Core I hope you’ve gotten a preview of what awaits at the end. Keep putting in the work, and I know you’ll be successful.

--

--