Teaching Software Engineering to Undergraduates


Last week the Software Engineering course I teach in the undergraduates Computer Science degree finished. I want to take this as an opportunity to reflect on the course, its motivation, its visions, its implications and its impact.

I took over the course from my predecessor (who retired in summer 2020) and redesigned and reshaped it completely from the ground up to my vision of what a modern, industry-level Software Engineering undergraduate course should deliver. I asked myself the question (based on my industry experience): How would I want to work in the industry? and shaped the course according to this. The good thing was that I had the opportunity to participate in the course as team coach during the last iteration of my predecessor, which shaped my vision and thinking of what I want and what I do not want to do.

The course happened in the 3rd semester of the Computer Science undergraduates degree. The relevant preceeding courses in the curriculum I built upon were (amongst others): programming in C (1st semester), OO programming in Java (2nd semester), Relational Databases (2nd semester) and Requirements Engineering (2nd semester). My course happend twice so far: once with 4th semester undergraduates in the summer semester 2021 and then again due to a change of the curriculum with 3rd semester undergraduates in the winter semester 2021/22 (this iteration).

The central vision of the course, I wanted to convey to the students was that Software Engineering is a learning process, working code a side effect. To simulate such a learning process, we set up a project in groups of 6-8 students, where they implemented a simple Hotel management software for a fake customer. Team members were selected by the curriculum administration, reflecting their marks so far in the curriculum: we tried to reflect the whole bandwith of marks, from excellent to poor, in the teams. Technologywise the project had to be a server-side rendered web application, implemented with Spring Boot, using Dependency Injection, using the HTML templating engine Thymeleaf.

The project was set up using Scrum, split into 7 Sprints, with the first one 3 weeks and the others 2 weeks. The project was divided into 4 phases:

  1. Product Vision Analysis and Requirements (a single 3 week sprint): weekly seminars with the customer and fleshing out of requirements using deliberate discovery techniques learned in this course, as well as requirements engineering techniques they learned in the previous semester.
  2. Setting up the architecture and first User Stories (2 sprints): in this phase the teams started with the implementation, by setting up the architecture layer by layer, where some groups used in-memory Repository implementations with fake data.
  3. Ading TDD (2 sprints): in this phase we expected the architecture to be fixed as well as the application connected to the database. The teams were expected to implement a wealth of new features following TDD principles and write tests for the existing functionality from the previous phase.
  4. Implementing an SPA on Top (2 sprints): in this phase we required the teams to implement a simple SPA on top of the existing server-side rendering application, to bring in an additional challenge as well as up-to-date UI/UX technologies. We left the choice of SPA library/framework to the teams, and most of them chose React.

The mark was split up into two parts:

  1. 70% project mark where the points were given to the whole team with the members distributing them amongst each other as they will.
  2. 30% individual oral exam.

Additionally there were weekly seminars to practise various topics. Also we had weekly 30-minutes coachings for each of the teams. At the heart of the course was the lecture delivered by me, where I taught the topics I discuss in the next paragraphs.

In addition to the Requirements Engineering techniques taught in the 2nd semester I introcued more hands-on deliberate discovery techniques: Impact Mapping, Event Storming, Behaviour-Driven Development which can be exercised properly only in a project setting, which follows up with implementations.

These are light-weight requirement techniques to help to understand the domain to implement. It is all about understanding first and of sharpening the focus, not to produce a complete requirements document, like a scope statement. My predecessor let the students do a comprehensive scope statement, with a fully fleshed out domain and DB model and fully formulated Use Cases for every feature. This had the consequence that the students (being students) did not really focus on understanding the domain and why the customer needs this solution (focus, business value) but purely on describing all requirements, without properly understanding them. This behaviour was obviously incentivised by making this scope statement a chunk of the final mark, so students did what I call check-box learning: they try to find out as quickly as possible what is necessary for a good mark and produce the result. Because understanding is not subject to a production process, as a consequence understanding the domain does not work this way.

Instead, using the above mentioned deliberate discovery techniques I could see that students arrivated at a much deeper understanding of the domain, and what problem they are solving for the customer. The seminars with the customer were quite lively and many discussions were happening between the customer and the teams - exactly the thing you want to elicit in an agile setting.

Teams also were requested to work on basic requirements artifacts such as User Stories, a first-shot Domain Model as well as UI/UX designs. However, they were not marked, but only seen as a starting point for when the implementation phase starts. This created a first shot and understanding for the teams, which was further iterated upon in the following sprints.

Students needed to start the implementation with a specific architecture, which is well suited for the problem and domain. Generally, in an agile process, one does some form of architectural prototyping if there is uncertainty as to which architecture might be suitable. In this course this is too early for the students, as they have just learned OO and do know nothing about architectures yet, nor do we have the time for such prototyping. Therefore, we decide for them for a monolithic 4-layered Hexagonal architecture (Presentation/Application/Domain/Infrastructure), implemented with Spring Boots Dependency Injection.

This kind of architecture is widely used and serves the purpose of a server-side rendered app very well, striking a balance between pragmatism and complexity. Yes, Microservices are probably the current state-of-the-art and hype but there is simply no way for 3rd/4th semester undergrads to touch them and their inherent, tremendous complexity (we treat Microservices architectures in a postgraduate course).

To implement the hexagonal aspect, the Inversion of Control container Spring Boot is used, which simplifies the whole configuration and set up tremendously. This also allows me to teach the fundamentally important concept of Dependency Injection in a properly applied context.

We implemented a fully agile process using Scrum. For that we coupled the project with another course in the same semester, called Agile Project Management by the amazing Peter Stadelwieser. In this other course they apply scrum and project management techniques to the software engineering group project such as selecting a Scrum Master, Daily Scrums, interacting with the Customer, creating estimations, writing User Stories, Retrospectives, Planing Poker, Sprint Planing,…

A thing which agile processes fundamentally accept is that each team is different, so the process has to be adopted to each team and adjusted slightly. We did the same thing, providing the teams with the broad goals but gave teams certain freedom on when they implement certain features, as well as how exactly they split the work amongst each other, and how properly they followed Scrum practises.

When you teach software engineering to students, there needs to be a big deal of the course devoted to Software Design. There exist various methodologies such as Object-Oriented Analysis and Design, however I believe that DDD is the most modern, and most industry-relevant methodology, worth teaching in such a context.

Obviously full-blown DDD with in-depth Strategic Design is far beyond the scope of an undergraduate course, so I only touched briefly on Bounded Contexts and put the main emphasis on the Tactical Patterns. The Tactical Patterns I cover in-depth: Value Objects, Entities, Aggregates, Domain Events, Domain Services, Factories and Repositories. I require students to design the Domain Layer of the 4-layered architecture following tactical DDD principles.

In the 3rd phase of the project students need to start writing tests, ideally in a TDD fashion. It is not possible to enforce a TDD approach in such a course, because you can always write tests after you implemented a feature, but I require a complete test coverage of the most important functionality: the essential methods of the Domain Model as well as all methods of the Application Layer including exceptions. What we are ignoring are any view-related tests.

In phase 4 the students need to implement an SPA on top of the existing architecture, therefore they are implementing a few RESTful endpoints. These I require the teams to test using Spring Boots helpful testing facilities.

Patterns are part of the standard repertoire of each Software Engineering course, as well as each developers toolbox in the industry. I have read and heard ocasionally opinions that (GoF) patterns are now obsolete and should not be taught anymore. To such opinions I can only say that they simply do not know what they are talking about and have problably never properly understood nor used design patterns in their life or on an industry level. I do not want to discuss in any depth why design patterns matter but the reason why I teach them in this course is that they are still (very) relevant in the industry and that it deepens the students OO skills and abstraction abilities and thinking tremendously. If someone has finished a Computer Science degreee and never heard of the Observer Pattern I seriously doubt the value of this education (no I do not want to get into a general discussion about the value of todays university system and education, which I agree is in some way fundamentally broken, but that is a topic for another blog post).

The patterns I taught are: Observer, Strategy, State, Composite, Visitor, Factory Method, Abstract Factory, Decorator, Adapter, Proxy, Bridge. I have selected them according to their relevance in the industry, based on my experience (which I argue is substantial). In the first iteration of this course (4th semester undergrads) I did simply present the GoF Patterns “theoretically” through slides, UML diagrams and code examples. This did not go very well and students had a hard time understanding them. In the 2nd iteration (the last one) I approached it much more interactively, stating the problem and motivation of each and then deriving the UML diagram, ending up implementing the examples in live coding sessions. The students loved it and took away much more.

Additional Topics I covered were Testing Theory (Black/White Box, Equivalence Classes); Refactoring; Technical Debt; SOLID Patterns; Code Quality, Coupling, Cohesion, Code Smells, as well as implementing Behaviour-Driven Development using Cucumber.

  • When you do group projects with teams you as lecturer arrange, award each team the points it deserves as a whole and let the team members figure out how they want to distribute the points among each other. This creates a feeling of justice for the team and a mechanism for teams to compensate for students who do nothing and only exploit the team. In case teams didn’t come to an agreement I would have used the oral exam mark as the final mark - all of the teams came to an agreement.

  • It is possible to deliver a highly challenging and difficult course from which the students are going to take a way a lot but that requires a substantial amount of effort from the lecturer in terms of personality, presence, vision, attention, supervision and experience. Also by giving the students certain freedom in how exactly they solve the problem, by appealing to their past learning experiences and their intelligence, makes them participate much more willingly in such a difficult course.

  • When teaching GoF Patterns, derive them step-by-step through UML diagrams and then an actual example implementation in code through live coding. This requires substantial experience, knowledge and effort from the side of the lecturer but the students will take away much more and enjoy it much more.

  • Students are able to pick up Spring Boot on their own very quickly (there was no introduction to Spring Boot in this course, nor in any other), and be productive in it as soon as they got the basic concepts.

  • There can be and are differences between each semester group. Despite being 1 semester “younger”, the 3rd semester group did remarkably well and even better, given the fact that they had only had Java programming for 5 weeks in the preceeding semester. I attribute this to different dynamics in each semester group, as well to the fact that with the 3rd semester group we could start on-site, which created a lot of momentum during the requirements phase.

  • One of the two hardest things for the students to understand is the 4-layered architecture. It is not immediately clear to them why splitting the system in such a way and how to implement it with Dependency Injection using Spring Boot. This requires a lot of coaching, supervision and explaining by the lecturer during coachings and seminars - but then students will get it and it will become pretty obvious to them

  • The other thing really difficult for the students to get and get right are the DDD Aggregates and Eventual Consistency. It is very difficult to think in terms of consistency boundaries, enforced through transaction boundaries because at that point they have not enough experience in OO and OO design. I think most of them have not properly understood Aggregates but I believe this is not an issue as some concepts take years to fully understand (say Monads in Haskell…). With experience comes understanding, and I am convinced as soon as they start working in the industry, it will click.

  • Another thing what struck me is how much different a mixed gender team works instead of an all-male team. It might sound like a cliche but I have seen it in this project first hand: whereas the male are more focused on the purely technical side of a software solution, the female students put a lot of emphasis on the interaction with the customer as well as the “human” / “social” side of the solution: UI/UX, requirements and others. I do not say that male or female are better or worse in either of these areas but I believe that their interests are distributed in that way - and this project gave first hand evidence to my belief. The consequence to me is that we need more women in technology and in Computer Science exactly due to this fact.

  • Most importantly: The teams which cooperated best, delivered the best solution, not the teams with the most/best technical talent.