The previous lesson fixed the overall scope of BiblioTech's final project and its chosen architecture. Before touching a single line of code, this lesson turns that scope into something much more concrete: a precise list of functional and non-functional requirements, and a plan of small iterations that avoids the temptation to try to build everything at once. Planning before coding isn't unnecessary bureaucracy: it's what lets you know, in Lesson 3, exactly what to build first and why, instead of moving forward blindly, assembling pieces in an arbitrary order.

Contents

  1. Functional requirements: concrete use cases
  2. Non-functional requirements
  3. Phased planning: four iterations
  4. Basic task estimation per iteration
  5. Final choice of technologies

  1. Functional requirements: concrete use cases

A functional requirement describes something the system must do: a concrete action a user (or an API client) can request and expect a result from. For BiblioTech, the final project's list of functional requirements is as follows:

# Use case Input Expected result
FR1 Add a book or magazine Title, author, ISBN (or other Book/Magazine data, Module 3) The item is added to Catalog
FR2 Remove an item from the catalog Item identifier The item no longer appears in the catalog
FR3 Search for items by title or author Search text List of matching items (LINQ, Module 4)
FR4 Add a member Member's name The member is added to Members, with a unique Id
FR5 Register a loan Item ISBN, member Id A Loan is created; the item is marked as unavailable
FR6 Register a return Loan identifier ReturnDate is assigned; a penalty is calculated if it's late (Module 8, IPenaltyPolicy)
FR7 Look up a book's external metadata ISBN Additional data from the external service (Module 5), or a clear absence if it's unavailable

Each of these seven requirements maps almost directly onto an existing method in Library, or a small combination of them — the table itself is, in effect, a map of which part of the code already built covers each requirement.

  1. Non-functional requirements

A non-functional requirement doesn't describe an action, but a quality the system must have while doing what it does. These are harder to verify with a simple use case, but just as important:

  • Reasonable performance: query operations (FR3, listing the catalog) must respond in an acceptable time even with a moderately sized catalog (hundreds or a few thousand items); no extreme performance goal is being pursued here, only avoiding obviously inefficient operations (for example, loading the entire catalog into memory on every request when a database query would be enough).
  • Basic security: the API must not needlessly expose sensitive data (for example, connection strings or internal database details in error messages returned to the client); production connection strings must live outside the source code (Lesson 5, per-environment configuration), never written directly in Program.cs.
  • Maintainability: thanks to Module 8's architecture (ILibraryRepository + dependency injection + unit tests), changing the persistence mechanism, adding a new endpoint, or fixing a bug should be possible without rewriting parts of the system unrelated to the change.
  • Resilience against external failures: the external metadata lookup (FR7) depends on a third-party service that may fail or not respond; Module 5 already showed this case is caught with try/catch around HttpRequestException, returning null instead of propagating the failure to whoever requested the metadata.

Unlike functional requirements, these aren't "marked as done" with a single one-off test: they're verified continuously throughout the whole project, and reviewed explicitly in Lesson 4's quality checklist.

  1. Phased planning: four iterations

Trying to build all seven capabilities from section 1 at once, with no order to it, is the most direct recipe for getting stuck halfway with nothing working end to end. Instead, the project is organized into four small iterations, each delivering something that works on its own before moving to the next:

flowchart LR
    I1["Iteration 1<br/>Domain + Persistence<br/>(already built, Modules 3-5-8)"] --> I2["Iteration 2<br/>API<br/>(Lesson 3)"]
    I2 --> I3["Iteration 3<br/>Client / UI<br/>(Lesson 3)"]
    I3 --> I4["Iteration 4<br/>Polish<br/>(Lessons 4-5)"]
Iteration What it delivers Requirements it covers
1. Domain + Persistence Confirming that Library, the domain model, and ILibraryRepository with its chosen implementation (section 5) compile and work in isolation, with no API yet Foundation of FR1-FR7 (already built in earlier modules, just verified to still be in order)
2. API ASP.NET Core Minimal API HTTP endpoints over the domain, with DI already registered FR1-FR7 exposed over HTTP
3. Client / UI A simple client (Blazor, or a console app with HttpClient) consuming the Iteration 2 API End-to-end verification of FR1-FR7 from outside the API process
4. Polish Integration tests, logging, quality checklist, deployment Non-functional requirements from section 2

This split into iterations isn't arbitrary: each one depends exclusively on the previous one, and each one is, by itself, verifiable — an API that responds correctly to curl, even with no client yet, is already a tangible result of Iteration 2, not half-finished work with no value of its own.

  1. Basic task estimation per iteration

An estimate doesn't need to be precise to the minute to be useful; a relative sense of effort is enough to help prioritize and to spot ahead of time which iteration is most likely to get stuck:

Iteration Main tasks Relative effort
1. Domain + Persistence Verify the four ILibraryRepository implementations (Module 8) still compile; choose one for production Low (already built, just verification)
2. API Solution structure, DI registration, 5-7 endpoints (one per functional requirement) Medium-high (the iteration with the most new code)
3. Client / UI One component or screen per main use case (catalog, loan, return) Medium
4. Polish Integration tests, logging, checklist, Dockerfile, per-environment configuration Medium

The iteration with the highest relative effort (the API) is precisely the one that leans most heavily on work already done in Module 8: most of that "load" is assembling and registering services, not new logic invented from scratch.

  1. Final choice of technologies

With the requirements now clear, it's time to fix, unambiguously, which specific technology will be used at each layer — summarizing decisions already presented or compared in earlier modules:

Layer Chosen technology Module where it was covered
Domain Existing C# classes (LibraryItem, Book, Magazine, Member, Loan) Module 3
Persistence abstraction ILibraryRepository Module 8 (Lesson 3)
Concrete production persistence Entity Framework Core over SQLite (EntityFrameworkRepository + LibraryDbContext) Module 5
Dependency injection ASP.NET Core's services container (AddScoped/AddSingleton/AddTransient) Module 8 (Lesson 3)
API ASP.NET Core Minimal APIs Module 7
Client Blazor (or a simple HttpClient consumer) Module 7, Module 5
Testing xUnit + Moq Module 8 (Lesson 4)
External metadata HttpClient against a simulated external service Module 5

Choosing Entity Framework Core (instead of text, JSON, or SQLite with raw ADO.NET) as production persistence responds to a concrete reason: it's the only one of Module 5's four options designed to scale comfortably as the data model grows, and it integrates naturally with ASP.NET Core's DI container via AddDbContext (already seen in 08-03, section 7). Nothing prevents replacing it with any of the other three at any point, however: that's precisely the guarantee ILibraryRepository offers.

Common Mistakes and Tips

  • Confusing functional requirements with implementation tasks: "use Entity Framework Core" is not a functional requirement (it doesn't describe what the system does for the user); it's a technical decision from section 5. Keeping the two separate avoids confusing requirement lists.
  • Skipping non-functional requirements because they seem "less concrete": they're just as real as functional ones, only verified differently (continuous review, a checklist) instead of with a one-off use case.
  • Planning iterations that depend on each other circularly: if Iteration 3 (Client) needed changes to Iteration 1 (Domain) to work, the plan has an ordering problem; check that each iteration only depends on earlier ones, never on later ones.
  • Tip: if an iteration looks "too big" when estimating it, that's a good sign it can be split into two smaller iterations, each with its own verifiable result.

Exercises

  1. For each of the seven functional requirements (FR1-FR7) in section 1's table, indicate which iteration from section 3 mainly covers it and which HTTP endpoint (verb + route) you'd expect to implement it in Iteration 2.

  2. A teammate proposes adding, halfway through Iteration 2, a new requirement: "allow reserving a book that isn't available, so it's held once it's returned." Explain why, according to this lesson's planning criteria, that requirement should be noted down for a future iteration instead of being added immediately to the current Iteration 2.

Solutions

Requirement Iteration Approximate endpoint
FR1 (add item) 2 POST /books
FR2 (remove item) 2 DELETE /books/{isbn}
FR3 (search) 2 GET /books?search=...
FR4 (add member) 2 POST /members
FR5 (loan) 2 POST /loans
FR6 (return) 2 POST /loans/{id}/return
FR7 (external metadata) 2 GET /books/{isbn}/metadata

Adding a new requirement halfway through an ongoing iteration breaks the central principle of phased planning: each iteration must deliver a closed, verifiable scope before moving to the next. Accepting the change right away would unpredictably lengthen Iteration 2 and delay the rest of the plan; the right move is to note the idea down (for example, as "Iteration 5: reservations," outside this final project's already-closed scope) and continue with what's already planned, just as would be done with any new functionality proposed during development, as already warned in the previous lesson.

Conclusion

This lesson has turned Lesson 1's overall scope into something much more concrete: seven functional requirements traceable to existing code, a set of non-functional requirements that are watched continuously, a four-iteration plan with clear dependencies between them, a relative effort estimate, and the final choice of technology for each layer. With this plan now closed, the next lesson, Implementation, stops planning and starts assembling: it will build the multi-project solution structure, register ILibraryRepository and Library in ASP.NET Core's DI container, and expose the endpoints corresponding to each of this lesson's seven requirements.

© Copyright 2026. All rights reserved