Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

35KB

FOCUS Architecture — Chapter-11: Features, Not Layers

  • Source: /library/FOCUS Architecture/source-file.pdf
  • PDF pages: 256–289
  • Pages without text: none

Features, Not Layers In this chapter, you’ll: sketch Rosie’s Coffee Shop’s folder tree from memory, with the five features and each slice’s four pieces in place; point, for any requested change, to the exact folder where the diff lands, before you open the editor; decide with an explicit criterion whether code should move up to shared/ , and refuse the promotion when reuse is still a bet. Chapter 10 handed you a four-piece map with a signed contract, and left you with a loaded question: which folder does each piece live in? You might think the answer is cosmetic, the kind of thing people argue about in a meeting over folder names. It’s the decision that sets the blast radius of every request Rosie makes from here to the end of the project. This chapter shows the organization that looks natural and charges dearly for it, the principle that replaces it, and the directory tree the rest of the book lives in. Chapter 10’s canonical table says what each piece does and forbids, but not where it lives. The path to that answer has three stops. First, the coffee shop organized the way most projects are born, which takes a real request and spreads the damage. Then the principle that explains why it hurt, with a name, a source, and

a diagram. Finally, the same coffee shop refactored, folder by folder, with chapter 10’s table getting a disk address and one deliberate absence you’ll notice before I explain it. The change that touched four folders Rosie’s Coffee Shop has five features: menu, tab, inventory, payment, and loyalty. Organized the way almost every project starts out, the folder criterion is the file’s technical type: views/ menu/ tab/ inventory/ payment/ loyalty/ controllers/ menu_controller tab_controller inventory_controller ... services/ menu_service tab_service inventory_service ... models/ menu tab inventory payment loyalty Each folder holds one kind of file, and each kind holds all five features mixed together. It looks organized, and it is: by the wrong criterion, as Rosie is about to demonstrate without meaning to. Her request lands on a Tuesday: “the customers at the table want to split the tab.” One feature, one sentence. The diff (the line-by- line difference between two versions of a file) that fills the request: --- a/models/tab.dart +++ b/models/tab.dart @@ class Tab

  • // The “split tab” change starts here: the tab starts tracking
  • final int people; --- a/services/tab_service.dart +++ b/services/tab_service.dart @@ class TabService
  • int valuePerPerson(Tab tab, int people) { --- a/controllers/tab_controller.dart +++ b/controllers/tab_controller.dart @@ class TabController
  • int splitTab(int table, int people) { --- a/views/tab/tab_view.dart +++ b/views/tab/tab_view.dart @@ class TabView
  • String renderSplit(Tab tab, int people) { Four folders for one sentence from Rosie. The model gained a field, the service gained the rule, the controller gained the pass- through, and the view gained the button. None of these edits is large; the problem is where they landed. The first pain shows up in review. Whoever reviews this pull request navigates four folders to understand a single intent, and between the + final int people; of the model and the renderSplit of the view sit dozens of menu, inventory, and loyalty files that have nothing to do with the change, but live along the way. The second pain shows up in the merge (folding two lines of work into the same file). While you were editing services/tab_service.dart , your teammate was adding this week’s promo to services/menu_service.dart , in the same folder. Both pull requests touch services/ , both compete for the same neighborhood, and the merge conflict is born between two features that don’t know each other. The third pain doesn’t show up in any tool, and it’s the worst one: no folder has an owner. Who’s responsible for services/ ? Everyone, because every feature has a file in there. A folder that belongs to everyone belongs to no one, and the question “who owns the tab?” has no answer on disk. The axis of change What hurt on Tuesday has a name. Axis of change is the criterion stating that things that change together should live together: you organize code by what changes in the same request, not by what looks alike technically. The tab model looks like the inventory

model, both are data classes; but the tab model changes together with the tab view, and never together with inventory. The resemblance is in shape; the change is in business. Applied to directories, the axis of change produces the vertical slice: the system’s cut that contains everything a feature needs, from view to repository. The cut crosses the layers top to bottom instead of lying flat over one of them. Jimmy Bogard gave this organization a name and an argument in “Vertical Slice Architecture” (2018): minimize coupling between slices, maximize cohesion within each one. The acronym VSA you’ll run into elsewhere is exactly this: Vertical Slice Architecture. The third name is the visible symptom of the other two. Feature- folder is the per-feature folder: features/tab/ with everything about the tab inside it. Keep the hierarchy among the three names in mind: it settles a critique further ahead, the principle is the axis of change, the cut is the vertical slice, and the folder is only the symptom. Whoever copies the folder without the principle carries the name and drops the benefit. The two diagrams below compare the two worlds. The first one is the layered organization: the tab change crosses all four folders, and every folder it crosses is shared with the other features.

The second one is the same change in slices: every arrow is born and dies inside its own folder.

The first one has four stacked boxes, one per layer, and each box announces it serves all five features at once; the arrow running from views/ down to models/ is the “split tab” change crossing shared territory. The second one has one box per feature, and the arrows link neighboring files in the same folder, without leaving it. It’s the same change; what changes is how many fences it jumps. The coffee shop in slices

No new project here: the tree below is the previous section’s coffee shop, refactored. The same files, relocated by the axis of change: features/ ├── menu/ │ ├── check_menu menu_orchestrator │ └── menu_repository menu_view ├── tab/ │ ├── split_tab │ ├── tab │ ├── tab_orchestrator │ ├── tab_repository │ └── tab_view ├── inventory/ │ ├── check_inventory inventory_orchestrator │ └── inventory_repository inventory_view ├── payment/ │ ├── check_payment payment_orchestrator │ └── payment_repository payment_view └── loyalty/ ├── check_loyalty loyalty_orchestrator └── loyalty_repository loyalty_view Two levels, and the second one is the file list in alphabetical order, the way your terminal prints it. The tab slice is open in full; in the other four the files sit side by side just to save lines. The tree is deliberately neutral: with no file extension, it holds equally for Dart, TypeScript, Java, or any of the book’s ten languages, and the names are about business and role, never about framework. What varies from one language to the next is the identifier’s case, and each one follows what it already uses in file names: tab_orchestrator in Dart, TypeScript, Go, Rust, and Python; TabOrchestrator in C#, Java, Kotlin, Swift, and PHP. It’s the same tree with the local convention.

Notice, too, what the tree doesn’t have: no shared/ folder, no utils/ , no place “for whatever’s left over.” The absence is a decision, not an oversight, and two sections from now you’ll see the criterion that decides when that folder earns the right to exist. Notice also what it doesn’t have on the inside: no subfolder. The four pieces of chapter 10’s canonical table are all in there, but as a filename suffix, not as a folder. The table never described folders; it describes the four pieces that exist inside each slice, repeated in every feature: Layer Does Forbids View fires events business rules renders state data access Orchestrator converts event to state deciding rules fetches data from the repository persisting calls use cases publishes state Use Case the only place for business rules IO is a pure function framework takes data, returns a Result domain exception Repository CRUD (fetch and save) business rules

the only place an infra exception becomes a Result Inside features/tab/ , each row has an address, and each address has a chapter that fills it in: Table row Path in the slice Who fills it in View features/tab/tab_view chapter 12 Orchestrator features/tab/tab_orchestrator chapter 13 Use Case features/tab/split_tab chapter 14 Repository features/tab/tab_repository chapter 15 Three of the four names follow the same formula, entity first and role as a suffix, and that formula is what replaces the folder: tab_view says what view/tab_view said, with one segment less and the same information. The fourth breaks the formula on purpose. The use case is called split_tab , a verb, not tab_usecase : every business rule turns into a file named after what it does, and the slice grows one file per rule. This chapter shows only signatures and names; the inside of each piece is chapters 12 through 15’s business, one per row, in the table’s order. There’s a fifth file missing, one that isn’t a table piece at all: the domain model. Tab and Item are the data the four pieces trade with each other, so they live in the slice that defines them, in features/tab/tab , next to the repository that reads and writes them. That’s the only file in the slice with no role suffix, and the absence is the mark: the bare name is the data. Don’t confuse this model with the persistence model, the one that mirrors the database table’s columns: chapter 10 already sent the persistence

model to be born and die inside the repository, and that ban still holds. What tab_repository hands to the rest of the slice is the domain model; the database’s shape stays in the box. There’s still the proof missing. The same Tuesday request, “split the tab,” redone on the new tree. Compare this diff with the previous section’s, file by file: it’s the same four edits, one per piece. --- a/features/tab/tab.dart +++ b/features/tab/tab.dart @@ class Tab

  • final int people; --- /dev/null +++ b/features/tab/split_tab.dart +// The “split tab” change lives entirely inside this slice. +int splitTab(Tab tab, int people) { --- a/features/tab/tab_orchestrator.dart +++ b/features/tab/tab_orchestrator.dart

@@ class TabOrchestrator

  • int onSplitTab(int table, int people) { --- a/features/tab/tab_view.dart +++ b/features/tab/tab_view.dart @@ class TabView
  • String renderSplit(int table, int people) { The diff didn’t shrink: it’s still four edits, because the change still needs a field, a rule, a sequence, and a button. What shrank was the blast radius. Every edit lives under features/tab/ , the reviewer opens one folder and sees the whole intent, the merge only conflicts with whoever else touched the tab, and the question “who owns the tab?” points to a folder with an owner’s name on it. Try it: run https://focus.kodel.com.br/en/dart/11-01 (or https://focus.kodel.com.br/en/ts/11-01): it’s the entire tab slice in a single file, with each piece’s path preserved in the comments. Delete the use case section and run it again. Predicted result: only the split breaks; menu, inventory, payment, and loyalty don’t even exist in the file, because none of them takes part in this change. The other eight languages are at the routes https://focus.kodel.com.br/en/java/11-01, https://focus.kodel.com.br/en/csharp/11-01,

https://focus.kodel.com.br/en/go/11-01, https://focus.kodel.com.br/en/php/11-01, https://focus.kodel.com.br/en/python/11-01, https://focus.kodel.com.br/en/kotlin/11-01, https://focus.kodel.com.br/en/swift/11-01, and https://focus.kodel.com.br/en/rust/11-01. Why it isn’t four folders By now the question has already formed: why doesn’t the tab slice have a view/ folder inside it, plus an orchestrator/ and the other two? The table has four rows, the slice would have four folders, each file would fall into its own, and the arrangement looks more serious than five loose files. The answer is that a folder is a promise of ownership, and a technical role has no owner. Apply two questions to any folder you think of creating, and both have to answer yes at the same time:

  1. Who named it? The cut is named by the business owner, in business words.
  2. What’s left if I delete it? Deleting the whole folder removes the cut from the system without touching a single file left in the slice. tab passes both. Rosie says “the tab” without anyone teaching her the word, and deleting features/tab/ takes the tab out of the system without menu, inventory, payment, and loyalty losing a line. view fails both. No business owner ever asked for “a view”, and deleting view/ would mutilate all five slices at once: each one loses its screen, none loses a whole capability. What cuts like that isn’t a business cut, it’s a file drawer.

The criterion has a practical consequence, and it is the slice’s growth mechanism. A slice that puts on weight doesn’t gain a drawer: it gains a sub-feature, a business cut with a folder of its own inside the slice, which holds every file that concerns only it and is flat on the inside the same way. If the inner cut grows, the criterion applies again, one level down. Suppose Rosie’s loyalty grows into two programs she names herself: the coffee stamp she punches on a paper card today and a monthly subscription club. Each one has its own screen, rule, and storage, each one is opened by the app’s route table, which lives outside the slice, and what’s left at the root is the customer both programs read. The tree looks like this: features/loyalty/ ├── coffee_stamp/ │ ├── coffee_stamp_orchestrator │ ├── coffee_stamp_repository │ ├── coffee_stamp_view │ └── punch_stamp ├── subscription_club/ │ ├── charge_club_membership │ ├── subscription_club_orchestrator │ ├── subscription_club_repository │ └── subscription_club_view └── loyal_customer Hypothetical is the word: the coffee shop that actually runs, the one in chapter 22, has its four slices flat, none of them with a sub-feature, because none reached that size. Two things to notice in the drawing. Both new folders are flat on the inside, with the same role suffixes in the names, and that is what it means to say the mechanism is recursive. And loyal_customer stayed at the root

because it belongs to neither program: deleting coffee_stamp/ takes the stamp out of the system and doesn’t touch loyal_customer or the club. The counterexample is more common than the example, and it is the cut that passes one condition and fails the other. Splitting the tab among the people at the table is named by Rosie in those words, so the first condition passes. The second one fails: the payment screen declares the screen of the split shares and instantiates it, so deleting a split_tab/ folder would leave payment_view pointing at a file that no longer exists. One condition alone isn’t enough, and the split stays flat in the payment slice, next to the rest. There’s a mechanical symptom of the same verdict: with the extra folder in the path, that file’s import runs past 85 columns, the limit this book imposes on its own listings, and it fits without it. The whole slice at once The folder criterion answers where each file lives. What’s missing is the measure of size, and it comes from a limit that didn’t exist when the architecture vocabulary was written. Whoever works with a language model works inside the context window, the total text the model considers at once, request and response together, counted in chapter 6’s tokens. Blow past the limit and something is left out, and what gets left out isn’t chosen by importance. The practical question that creates for a project’s design is a blunt one: which unit of reading answers a whole task without blowing the window? The flat slice is this chapter’s answer. To touch the tab’s discount, what has to come in are the five files in features/tab/ and the contracts it uses, and nothing else in the system has to come

along. In the layered organization the same task means opening five distant folders and carrying, in each one, the files of the other four features that live there by accident of technical role. The cost isn’t aesthetic: it’s the number of irrelevant things taking up the window before the question gets answered. The property has a name: context locality, what changes together being close enough to be read together. It isn’t a new concept in this chapter, it’s the axis of change measured with another ruler. The axis of change asks what changes for the same reason; context locality asks how much you read to answer that change. When the cut gets the first one right, the second comes along, and the same slice that fits in the window is the one that fits in the head of whoever joined the team yesterday. What the imports give away You don’t have to take a diagram’s word for it: the compiler records coupling in text, right in every file’s header. The listing below compares the tab view’s imports under the two organizations. Both languages write identical paths; the only mechanical difference in TypeScript is that the path drops the .dart and the line gains braces around the imported name, as in import { TabService } from “../../services/tab_service” . Dart · TypeScript // Group 1: layered, the header of views/tab/tab_view.dart. The view // climbs two levels and crosses the project to find what it uses. import ‘../../models/tab.dart’;

import ‘../../services/tab_service.dart’; // Group 2: sliced, the header of features/tab/tab_view.dart. The view // imports one neighbor only: the orchestrator that publishes the state // it draws. import ‘tab_orchestrator.dart’; Read the paths as arrows from the axis-of-change section’s diagram. Every ../.. in the first group is an arrow crossing the diagram end to end: the view lives in one folder, climbs to the root, and dives into another folder shared by all five features. The second group’s import has no path at all, just a filename: what it uses sits in the same folder, inside the slice’s fence. Notice what the second group doesn’t import, and notice that distance has nothing to do with it anymore. tab_repository and split_tab sit in the same folder, one name away, and they stay out of the view’s reach: the View row forbids data access and forbids business rules, and the ban belongs to the contract, not to the geography. In the tree with drawers the two were easy to confuse, because writing ../data/ looked expensive. In the flat folder the shortcut is cheap, and the only thing barring it is chapter 10’s table. Chapter 12 devotes a whole pitfall to the day this import shows up. The imports’ distance measures coupling between folders, and that gives you an audit trick that works on any project, yours or someone else’s: open half a dozen files and look only at the headers. Short, neighboring imports say that what changes

together lives together. Headers full of ../../ crossing the project say the axis of change and the folder tree disagree, and every simple request is about to cost a crossing. The same slice in ten languages The slice isn’t a Dart idea, or a TypeScript one. The ten implementations of snippet 11-01 carry the same tree in their path comments, and the spot where it becomes visible on a single screen is the orchestrator: it calls split_tab and asks tab_repository for the tab, two files that sit right next to it. Three names, two lines of code, not one step outside features/tab/ . Start with the two languages that opened the chapter. Notice that the use case file has a verb for a name and stands alone, with no class wrapped around it: Dart // features/tab/split_tab.dart int splitTab(Tab tab, int people) { var total = 0; for (final item in tab.items) { total += item.priceInCents; }

return total ~/ people; } // features/tab/tab_orchestrator.dart class TabOrchestrator { TabOrchestrator(this.repository); final TabRepository repository; int onSplitTab(int table, int people) { return splitTab(repository.fetchTab(table), people); } } TypeScript writes the same neighborhood with two mechanical swaps: integer division becomes Math.trunc , and the dependency enters through a field assigned in the constructor. TypeScript

// features/tab/split_tab.ts function splitTab(tab: Tab, people: number): number { let total = 0; for (const item of tab.items) { total += item.priceInCents; } return Math.trunc(total / people); } // features/tab/tab_orchestrator.ts class TabOrchestrator { private readonly repository: TabRepository; constructor(repository: TabRepository) {

this.repository = repository; } onSplitTab(table: number, people: number): number { return splitTab(this.repository.fetchTab(table), people); } } The other eight write the same neighborhood, and repeating the whole slice ten times would just repeat the same tree in different syntax. Below is only each one’s orchestrator, which is where the three names meet. In every one, look for the same two things: the path in the comment and the use case’s name called with no folder qualification at all. Kotlin shrinks the orchestrator down to a primary constructor, and the dependency is declared on the class’s own line: Kotlin // features/tab/TabOrchestrator.kt class TabOrchestrator(private val repository: TabRepository) { fun onSplitTab(table: Int, people: Int): Int {

return splitTab(repository.fetchTab(table), people) } } Swift uses a struct with let : the dependency is an immutable property, and the initializer comes for free, with no line written for it. Swift // features/tab/TabOrchestrator.swift struct TabOrchestrator { let repository: TabRepository func onSplitTab(table: Int, people: Int) -> Int { splitTab(repository.fetchTab(table: table), people: people) } }

C# spells out the constructor in full, and the use case needs a class wrapped around it, because C# has no standalone functions. The file still has a verb for a name, and the call SplitTab.Execute shows the neighboring folder right in its own name. C# // features/tab/TabOrchestrator.cs class TabOrchestrator { private readonly TabRepository _repository; public TabOrchestrator(TabRepository repository) { _repository = repository; } public int OnSplitTab(int table, int people) { return SplitTab.Execute(

_repository.FetchTab(table), people ); } } Java has the same restriction as C# and solves it the same way: the rule is a static method inside a class with a verb for a name. Java // features/tab/TabOrchestrator.java class TabOrchestrator { private final TabRepository repository; TabOrchestrator(TabRepository repository) { this.repository = repository; }

int onSplitTab(int table, int people) { return SplitTab.splitTab( repository.fetchTab(table), people ); } } PHP goes back to a standalone function for the rule, and PHP 8’s constructor promotion declares the dependency right in the signature. PHP // features/tab/TabOrchestrator.php final class TabOrchestrator { public function __construct( private readonly TabRepository $repository,

) { } public function onSplitTab(int $table, int $people): int { return splitTab( $this->repository->fetchTab($table), $people, ); } } Python is the only one that splits the fetch from the decision into two named lines, and that leaves the orchestrator’s sequence literal: the data first, the rule after. Python

features/tab/tab_orchestrator.py

class TabOrchestrator:

def init(self, repository: TabRepository) -> None: self._repository = repository def on_split_tab(self, table: int, people: int) -> int: tab = self._repository.fetch_tab(table) return split_tab(tab, people) Go has no classes. The orchestrator is a struct with a receiver method, and the dependency is the Repository field, filled in by whoever assembles the graph. The folder neighborhood is identical. Go // features/tab/tab_orchestrator.go type TabOrchestrator struct { Repository TabRepository } func (o TabOrchestrator) OnSplitTab(table, people int) int {

return SplitTab(o.Repository.FetchTab(table), people) } Rust splits data from behavior into two blocks, struct and impl , and &self.repository makes the borrow explicit. The path in the comment stays the same. Rust // features/tab/tab_orchestrator.rs struct TabOrchestrator { repository: TabRepository, } impl TabOrchestrator { fn on_split_tab(&self, table: i64, people: i64) -> i64 { split_tab(&self.repository.fetch_tab(table), people) } }

Ten syntaxes, one tree. None of the ten needed a common folder to work, and that absence is what the next section is about. shared/ is born empty Time to pay off the tree’s promise: where’s shared/ ? Nowhere, and the rule governing that is the chapter’s fourth and last concept. The late shared/ rule says the shared/ folder is born empty and absent, and that code only moves up to it once reuse has proven itself in at least two real slices, written and working. Two, not one and a half: as long as the second slice that needs the code doesn’t exist on disk, the code stays in the only slice that uses it. The trigger is chapter 5’s rule of three: wait for evidence of repetition before you abstract. The foundation is chapter 4’s YAGNI: don’t build for the need you imagine, build for the one that showed up. I carry a scar that backs up this rule, and I’d rather tell it than fake neutrality. On a project that passed through my hands, the shared/ folder was created on day one, before the first feature, “because we’re going to need it.” Two years later it was the system’s biggest source of coupling: forty-something files that every feature imported, where any change demanded testing the whole app, and where every ownerless piece of code got pushed, because the common folder is the path of least resistance. No slice had a fence, because all of them had a tunnel to the same basement. The folder born to avoid duplication turned into the place every change leaked through. Two things the word “folder” runs together are worth pulling apart, because this chapter’s rule looks like it bans both and bans only one. The drawer it bans is a technical-role folder repeated inside every slice, and its damage is cutting one business capability into pieces: with it, a change to the tab jumps four fences inside the tab itself. shared/ and the root infrastructure

folder don’t do that. Both sit outside features/ , both exist once in the whole project instead of once per slice, and neither cuts any capability: shared/ holds what two slices proved they have in common, and infrastructure holds the wiring nobody asks for in business words, the route table, the dependency graph, the entry point. The axis of change backs them up, because what lives in them changes for its own reasons and not alongside a feature. What doesn’t change is the timing: shared/ is still born late, with reuse proven in two real slices. By now you’ve probably heard the two classic critiques of this way of organizing, and both deserve an answer: The first: “organizing by feature is just reorganizing folders, and the same layers stay inside every folder.” Oskar Dudycz published that objection in “My thoughts on Vertical Slice Architecture” (https://www.architecture-weekly.com/p/my- thoughts-on-vertical-slices-cqrs): for him, this just relocates the layered structure into each feature folder: the same over- engineering, rearranged into new drawers. He’s right, and the tree you read in this chapter is what that critique produced. The design I used to defend before it put view/ , orchestrator/ , usecases/ , and data/ inside every slice, and I called that a vertical slice. It was the objection’s exact target: the four layers were still there, with the same mandatory crossing, only multiplied by five, one copy per feature. Read that way, it isn’t an objection to the vertical slice; it’s an objection to whoever copied the folder and left the principle behind. The four folders became four name suffixes, each file’s role is still declared, and the crossing is gone. What the objection doesn’t reach is the rest. If the tree were the principle, there’d be nothing left to answer; but the tree is the symptom, and the principle is the axis of change, which also sizes the slice from the inside. A CRUD with no business rule

gains no use case file at all: the slice keeps the view, the orchestrator, and the repository, and chapter 10’s table still stands as the contract for what the use case would do if it existed, ready for the day the first rule shows up. A slice isn’t a four-piece mold; it’s the cut of what changes together, whatever size the feature calls for. The second critique: slices duplicate code and fragment the system, because each one rewrites what could be shared. The answer is the rule that opens this section. Once reuse proves itself in two slices, the code moves up to shared/ with a business name and the duplication dies; until it proves itself, temporary duplication is cheaper than the wrong abstraction, as chapter 5 argued with Sandi Metz. Bogard himself, in “Vertical Slice Architecture” (2018), treats coupling between slices as the cost to minimize; a premature shared/ is exactly that cost, installed wholesale on day one. Pitfalls The first pitfall is a premature shared/ , the same one from the scar. What goes wrong: every slice ends up depending on the common folder, and any change to it ripples through the whole system. Why: reuse was guessed instead of proven, and guessing at reuse errs on the expensive side. How to get out: return each piece of code in shared/ to the one slice that actually uses it; whatever’s left, used by two or more, has earned the right to stay. The second is the utils/ folder, which starts with two date functions and turns into the project’s junk drawer. What goes wrong: utils/ has no axis of change at all, so everything fits in it, and what fits everywhere belongs nowhere. Why: the name is technical and empty, it says nothing about which business the code is for. How to get out: every function in utils/ either belongs

to a slice and goes back to it, or has proven reuse in two slices and moves up to shared/ with a business name, like price_formatting , never helpers . The third is slicing by screen instead of by feature. It looks the same, until the day features/tab_screen/ and features/split_tab_screen/ change together on every single request, because they’re the same feature cut in two. That’s the symptom: two slices that always show up in the same diff. The fix is to merge them and give the slice the business name, tab , with as many screen files as it needs: tab_view , tab_list_view , one per screen. Q&A What if one slice needs another? Payment checks loyalty to apply the discount. The case exists and has an address: the collaboration happens in the payment orchestrator, which asks for the points through the loyalty slice’s public contract, never importing one of its internal files. The fine- grained design of that conversation belongs to chapters 13 and 14; for now, keep the pocket rule: a slice talks to a slice through the front door. My project is small, three screens. Do I need this? You need the criterion, not the ceremony. Three screens in three slices cost three folders, the same number of folders views/ , controllers/ , and models/ would cost, and they save you a change of address the day the project grows. No subfolder comes along for the ride: a small slice stays small. Does a plain CRUD need all four pieces? No. With no business rule, there’s nothing to write in a use case file, and the slice keeps the view, the orchestrator, and the repository. Chapter 10’s table keeps being the contract: when the first rule arrives, you’ll know exactly which file to create and what it can and can’t do.

Quick tip In your next code review (another person’s review of the code before it merges), ignore the body of the files for one minute and read only the list of paths touched in the diff. If that list doesn’t fit inside one feature folder, you just found the project’s real axis of change, and it disagrees with the tree. Quick reference Situation Fix Screen or text change features//_view New or changed business rule features//_ , one per verb New field from the API or database features// and its repository New sequence from event to state features//_orchestrator Large business cut inside the slice a folder with the name the owner uses; reread the two conditions Same code in two real slices shared/ candidate, with a business name Code that “might” get reused stays in the slice that uses it; YAGNI (chapter 4)

The urge to create utils/ reread Pitfalls Exercises

  1. Close the book and sketch Rosie’s Coffee Shop’s tree: the five slices and the five files of features/tab/ , with the name this chapter gave each one. Check it against the coffee-shop-in- slices section; any folder you invented inside the slice is the drawer coming back, any file you left out is the table row you haven’t linked to a name yet.
  2. Place three of Rosie’s requests, one file each: “two-for-one coffee promo on Thursdays,” “a new digital wallet payment method,” and “low-stock alert.” Answer key: the promo is a menu pricing rule and turns into a verb file in features/menu/ ; the digital wallet is a new way to pay and lives in features/payment/ (the rule in a verb file, the integration in payment_repository ); the alert belongs to inventory and lives in features/inventory/ .
  3. Price formatting in dollars today exists only in the tab’s view. Should it move up to shared/ ? Decide and say the criterion out loud before you check: it doesn’t move up, because reuse hasn’t proven itself in two real slices yet; the day the menu view needs the same formatting, the two slices prove the reuse and the function moves up with a business name. Tip 11 Organize by what changes together, not by what looks alike. Next chapter: chapter 12 lifts the table’s first row off the page: the View, the piece the user touches, built inside features/tab/tab_view without carrying a single rule.

Powered by TurnKey Linux.