MVC Architecture and Coding Best Practices
Mapping MVC layers onto a TicTacToe codebase. Package-by-layer conventions and the two non-negotiable coding rules for LLD.
A class diagram tells you what entities exist. It does not tell you how to organize them into a codebase or where business logic should live. That is the job of the architecture. For TicTacToe, we use MVC (Model-View-Controller), the standard architecture for almost any LLD interview problem.
The Essentials
- MVC is the Restaurant: View is the Customer, Controller is the Waiter, Model/Service is the Kitchen.
- Each layer has one job: Controllers route, Services run logic, Repositories handle data, Models are pure state.
- Package by Layer: Group files by their architectural role (
models/,controllers/, etc.). - Private Attributes Only: No public fields. All access goes through getters/setters for validation control.
- Initialize Everything: Every attribute must be non-null after the constructor. Initialize lists to empty, not null.
MVC: The Restaurant Analogy
ExpandMVC Flow Diagram
The clearest way to understand MVC is through a restaurant.
- The Customer (View): They place an order (request) but have no idea what happens in the kitchen. In our game, the
mainloop is the customer. - The Waiter (Controller): They take the order, pass it to the kitchen, and bring back the result. The waiter does not cook. They just route. This is our
GameController. - The Chef (Service/Model): They know the recipe (business logic). They decide which ingredients to use and in what order. This is where our game logic lives.
- The Storage (Repository): This is where ingredients are kept. In a simple game, we don't need a database, but in a production system, this would be our data layer.
The Thin Controller Pattern
In a clean design, the controller should be "thin." It should receive a request, call a method on a model or service, and return the result. It should contain zero business logic.
class GameController {
makeNextMove(game: Game): void {
game.makeNextMove();
}
}If a controller method is longer than 15 lines, business logic has leaked into the wrong layer.
Coding Best Practices
Two rules apply without exception when I write model classes:
Rule 1: Every attribute must be private
Public fields are a red flag. Private attributes with getters and setters give you control. You can add validation (e.g., "row must be between 0 and size") inside a setter without changing any other part of the codebase.
Rule 2: Initialize every attribute in the constructor
The fastest way to cause a NullPointerException is to leave an attribute uninitialized. If an attribute is a list, initialize it to an empty list in the constructor. If it is an object that will be set later, initialize it to null explicitly and document why.
class Game {
private board: Board;
private moves: Move[]; // Initialize to [] in constructor, never null
private status: GameStatus;
constructor(board: Board) {
this.board = board;
this.moves = [];
this.status = GameStatus.IN_PROGRESS;
}
}Why Not Singletons, Why Not Static Methods
Two questions always come up in LLD interviews regarding the Controller:
Should the controller be a singleton? Only if having more than one instance causes a real problem -- like a shared resource or a performance bottleneck. The game controller has no state and no shared resources. Singletons also make unit testing significantly harder. I prefer instance methods unless there is a genuine reason to restrict creation.
Should the controller methods be static? It’s tempting, but it’s a bad habit. Real-world controllers have dependencies (services, loggers, exception handlers). Static methods force you to make those dependencies static too, which pollutes the global namespace and makes the code harder to mock in tests.
Exception Handling: Desktop vs Web
In our desktop game, when validation fails, the builder throws an exception. The controller propagates it, the main method catches it and prints a message. This works because the client and controller share the same JVM.
In a real web system, this approach breaks down. The controller (Java) and the client (JavaScript) are separate processes. The controller must intercept internal exceptions and "translate" them into well-formed HTTP responses -- a 400 for bad input, or a 404 for not found. This prevents leaking your internal class structure to the user through a stack trace.
Package by Layer
Standardization is key. I always use this structure in interviews:
models/: Game, Board, Cell, Player, Symbol, Move.controllers/: GameController.strategies/: WinningStrategy, BotPlayingStrategy.enums/: CellStatus, GameStatus.exceptions/: Named exception classes.
This structure communicates the architecture more directly than scattering files. It shows the interviewer that you have a consistent, professional approach to organization.
With our architecture set and our rules established, we are ready to open the editor. In the next post, we will start coding the game loop and the controller.
Further Reading and Watching
Practice what you just read.
Keep reading