Winter 2025

Full-Stack Engineer at School of Computer Science

This has turned out to be my favourite work term by far, providing me with the opportunity to work on a completely fresh project from scratch with a small team. I was able to work with many different aspects of the development cycle including the initial user stories, system design, actual implementation, and testing throughout the work term. By the end of the work term, I successfully brought the project to a state where I and the rest of the team were happy with handing it off to the next co-op. It was a full minimum viable product and had met all of the initial requirements we found, and I could not have been more proud of what I was able to accomplish!

1. A New Project

For this work term, I was brought on to work on a completely new project. The project was a complete rework of an existing piece of software used by SOCS, but was intended to be written entirely from scratch in a new tech stack to make it more extendable in the future. This was completely different from anything I'd ever worked on before in a professional setting, because before I had always been given existing code to work off of - which was not the case here. Instead, I was only given the tech stack that the project should be built on, and was able to work with my supervisors and the rest of the team on designing everything from the ground up. The team I worked with consisted of my two supervisors and two other SOCS staff who were working on the project alongside their existing work. We also had a faculty supervisor on the project. The support and guidance of the entire team was invaluable to me throughout the development cycle, and I always felt that if I was confused, I had someone I could ask to clear things up. At the same time, I always felt that my opinions and suggestions were important to the project, which was really nice and really gave me the sense that my work was impactful.

2. Working with the Users

The first thing I got to do for this project was meet with our users and learn what they wanted from the new service. I combined all of this into a set of user stories that would guide development going forward. I worked with two other team members (Shaiza and Sarah) to make some of the initial low-fidelity wireframes for the system. They then improved on these wireframes to make a high-fidelity prototype, giving me a chance to see how a prototype evolves from a very rough initial concept to a fully fleshed-out wireframe. After a few weeks of making these prototypes, I participated in five prototyping sessions with various future users representing several user groups. These sessions gave me a chance to really see how a prototyping session can impact a project and guide improvements. Before this, I had only participated in prototyping for courses at the university, but never for an actual project. It was fascinating to see the variety of opinions expressed by our testers and get the chance to sift through the feedback to find the most important information from it.

3. Designing a Complex System from Scratch

This was my first chance to participate in the initial design of a complex production system. The service we were developing was intended to be extendable with more features in the future, so our design had to be modular enough to support such changes. I started off knowing a few things: what technology we'd be using (Django for the backend), and that it should be using a microservices architecture. With that as a starting point, I started off by sketching diagrams of multiple potential system architectures and presenting them to the team for feedback. This feedback was important to me for both learning better ways of designing the system, but also for narrowing down the many ideas I had of how to do it. Likely the most complex part of the architecture was the database. I tend to think of programming in object-oriented terms, which doesn't fully map to good database design practices. I worked with Shamsi, one of my supervisors, as he was drawing out entity relationship diagrams for the database structure. I initially just watched and gave a few ideas I had based off of my limited knowledge, but slowly starting learning the basics of database design and giving more useful suggestions. It took a long time to get this right - we presented the design to Judi, our faculty supervisor a number of times and went away with many changes to make each one of those times. But these presentations were also amazingly helpful learning experiences for me as I learned best practices in structuring databases. In the end, we settled on a database schema that was almost entirely different from the initial diagram we made, but was considerably more stable and extendable. From all of this, I took away a good lesson: Design is an iterative process, not just a one-and-done task. A good design will be iterated upon as issues are found throughout the development process, so it should never be considered "finished" until the entire product is complete.

4. Testing and Code Quality

One of my early tasks was setting up CI/CD for the project to ensure high code quality and code correctness. This included picking code quality tools, writing out a style guide, and setting up automated testing. I spent a few days researching various tools that could be used, and learned about the different kinds of tools we'd need. In the end, I settled on a tool for each kind of quality check we'd need: formatting, linting, and testing. 1. Formatting: This is akin to a code style guide. A good project has a consistent code style throughout, which is best achieved by having an automatic code formatter. I picked a formatter called "Black", a widely used Python formatter that is known for being highly opinionated. Though it took a little while to accept that my code would be changed to match Black's intended style, I eventually found myself liking the assurance that all the code in the project would follow a standardized ruleset for indentation, line splitting, and other format details. 2. Linting: A linter is a tool that checks for common mistakes that aren't necessarily bugs, but are bad practice. I picked a tool called "Ruff", a very fast Python linter with a wide selection of various rules that could be enabled. I spent a few hours looking through its catalogue of rules and picking ones that would be best for our project. This was very helpful as the project grew in size, as it would automatically detect any leftover "TODO" comments, debug print statements, and even improper usage of language features (instances where a feature is ambiguous and sounds like it does one thing, but actually does something else). 3. Testing: This is the kind of code testing that I was most familiar with earlier. I set up unit and integration tests using Django's default test runner. The most difficult task here wasn't the tool selection or setting it up, but actually staying on top of writing tests and ensuring the tests are easily editable in the future. I learned that it can be really helpful to create a suite of data mocking *methods* that I could use to generate fake data on the fly for each test. As the project grew, it saved me probably hundreds of hours of time when I could just call a method to set up the database with configurable parameters before each test case, rather than manually generate all the data needed for each test. This also coincidentally helped generate mock data for manual system tests, because all I had to do was call one method to generate a whole set of data based off of rules that I could easily have forgotten. I also learned a little bit about the paradigm of test-driven development. Though I didn't try applying it to my initial implementations of features, I used it whenever I was fixing a bug. I would first identify a minimal reproducible case of a bug, write a test to automate checking it, and then find ways to fix it afterwards. This ensured that I could very quickly iterate my solutions, as I could just re-run that one test case and see if my fix worked.

5. Keeping Notes and Staying on Schedule

Something new I tried in this work term is daily notetaking. I would keep a separate to-do list for every day of the work term, along with a list of things I'd done that day. At the end of the day, I created a new page for the following day and copied over unfinished items, added new items, and generally edited the to-do list so I could have a properly prioritized list of tasks for the following work day. I found this to be extremely helpful with staying on top of my work. It meant that I could always come back to work and just check my to-do list to see exactly what I had to do that day, instead of having to remember what I was working on when I left off. If I wasn't sure of the reasoning for something, I could go back to the previous day's notes and see what I did and why I did it. This helped me separate work from the rest of my life, and I didn't need to think about work at all after I left for the day - I was safe in the knowledge that whatever I needed to do was safely recorded in my digital notes.

6. Learning about API Design and New Technologies

One of the biggest things I learned this work term was good RESTful API design, and how to implement it in Django Rest Framework (the technology we were using for the backend). Previously, I had thought of RESTful APIs as just any API where no state had to be maintained between transactions. But while that is at the core, there really turns out to be a lot more subtleties and nuances to designing a good API with this approach, and some instances where it isn't the best approach in the first place. First, a RESTful API does have to be stateless - an operation should not depend on some backend state being set other than the specific data it is working with (for example; it shouldnt't have a list of ongoing connections). A RESTful API is also resource-based to enable high flexibility with how it is used. Every endpoint should define a resource that can be viewed, and the available HTTP methods indicate actions to be performed. This can be contrasted with action-based endpoints where every endpoint defines a specific action being performed (for example, "POST /users/" vs "/users/create_user/"). This caused some confusion for me for a while as it doesn't match with how I think of the data our system used. I thought of many resources as being related to others and this accessed through those relations, but many truly RESTful APIs have full separation of all resources from one another to enable better modularity in the design (eg. "/users/1/orders/5/ vs "/users/1/" and "/orders/5/") Overall, it was a great learning experience to work with Django Rest Framework and learn how to design RESTful APIs through hands on experience!

7. Conclusion

Overall, this was a fantastic work term. I think with the support and guidance of the team, and the opportunity to experiment and try different approaches, I was able to learn far more in a vast variety of different areas than in any other previous work term. I went away from this work term with new skills in software quality and testing, deployment, CI/CD, RESTful API development, Django development, database design, and more. I am extremely grateful for having this opportunity and I'm proud of the work I completed over the course of the winter!