1. Reader and Writer Are Options, Not Architecture Rules

    Choose Reader, Writer, and tool-layer boundaries from code size, domain distance, legacy constraints, and team conventions—not imitation.

  2. The Common Module Trap: Organize Code Around Concrete Capabilities

    Delay common modules until ownership is clear, and organize logging, exceptions, and shared protocols around concrete capabilities instead.

  3. How to Break Up a Giant Service Class Without Guesswork

    Split an oversized service by mapping concepts, tracing responsibilities, testing boundaries, and comparing concrete refactoring options.

  4. Keep Operational Tables Clean by Building a Separate Query Model

    Design operational tables around business concepts, then serve complex history and admin searches from a separate query model.

  5. One Database, Separate Boundaries: Controlling Data Access by Domain

    How separate entity and repository boundaries can protect core rules from integration code even when both areas share one database table.

  6. What a Utility Bill Can Teach Us About Domain Modeling

    How a utility bill revealed a missing delinquency concept in a repayment model and why everyday artifacts can guide software design.

  7. Rank Your Domain Concepts Before You Spend Design Effort

    Identify a small first tier of domain concepts, separate supporting flows, and spend limited design time on the service's center.

  8. Before Fixing Circular Dependencies, Fix the Domain Model

    Resolve confusing ownership and concept boundaries before using interfaces or dependency inversion to treat an apparent circular reference.

  9. Create a Deliberate Messy Boundary to Keep the Core Clean

    Contain unavoidable complexity in an explicit outer boundary so limited engineering time can protect the system's core concepts.

  10. Layering That Teammates Can Understand—and the Build Can Enforce

    Define stable layer roles, constrain optional upper layers, and automate checks only when a team needs stronger enforcement.

  11. One Project, Many Deployables: Drawing Boundaries by Runtime Role

    Separate public, admin, batch, and operations workloads into runnable applications without splitting the whole codebase into separate projects too early.

  12. Name Readers, Finders, and Searchers by Behavior

    Distinguish Reader, Finder, and Searcher classes by direct reads, added filtering, and composite searches rather than result count.

  13. Measure Before Replacing Outbox Polling

    Evaluate outbox polling with realistic load tests, then consider a simpler application-level delivery path before adopting log tailing.

  14. Publish Events Where the Business Flow Is Visible

    Place event publication in the layer that understands the completed business action, while keeping implementation-specific cases explicit.

  15. Compose My Page Data Without Inventing a My Domain

    Build My page summaries through a dedicated composition layer while keeping user, order, and product responsibilities in their proper domains.

  16. Modules, Layers, and Architecture Are Different Decisions

    Separate module boundaries from code-level layers and architectural style, then extract modules only when implementation needs stronger constraints.

  17. Earn Your Interfaces: Abstraction After Evidence

    Start with concrete code, extract interfaces from proven variation, and keep one-to-one abstractions only when they create a real boundary.

  18. Choose Hexagonal Architecture for a Reason

    Choose hexagonal architecture when protocols, isolation, scale, or likely growth justify its extra structure—not because the label signals skill.

  19. Authorization Without Redundant Reads

    Resolve the logged-in user once, keep access checks focused, and separate system-wide administration from channel-level permissions.

  20. Design for Change with Core Ownership and Surrogate Keys

    Centralize a shared registration rule in the core, escalate isolation only when needed, and keep changing business uniqueness out of the primary key.

  21. Separate Shared Counts from Personalized Cache State

    Compose a reaction status from reusable queries, then cache the shared count without leaking one member’s personalized state to everyone else.

  22. One Read Model for Guests and Members

    Return public reaction counts and member-specific state through one read flow while keeping optional identity distinct from required authentication.

  23. Rebuild First, Migrate Second

    Design a replacement system around the problems it must solve, then handle legacy data through explicit migration mappings and retirement conditions.

  24. Modeling Reactions from Requirements to Scale

    Turn a vague like-button request into a reaction model by clarifying policy, estimating data growth, and planning when count queries must change.

  25. Designing Domain Models Beyond the Shape of the UI

    A tree-shaped API does not require a tree-shaped domain. Keep the client contract at the presentation boundary and compose it from internal concepts.

  26. Reversible Design Is the Antidote to Overengineering

    Build the requirement in front of you, look only a little beyond it, and keep speculative structures cheap to expand, remove, or replace.

  27. Win Technical Arguments with Small Proofs, Not Opinions

    Diagnose why a proposal is blocked, build a small proof, strengthen your reasoning, understand the opposing case, and treat new technology as company risk.

  28. Validate at the Boundary, Simplify the Core

    Turn API input into complete business values at the presentation boundary, keep nulls out of inner flows, and validate stored data when it enters the system.

  29. Keep API Request Models Out of the Core Domain

    Convert external request objects into business-owned values at the presentation boundary so the API depends inward and the core never depends back.

  30. Give Cross-Domain Behavior to the Domain That Owns It

    Before placing a caller-owned interface across modules, identify which domain owns the action and let callers depend on that capability.

  31. Who Owns the Enum? Dependency Design Across Domain Modules

    Put a business enum with its domain, let storage depend inward, and use a small shared enum module only while the domain boundary is still emerging.

  32. Provider Identity and Authentication Boundaries for Platform APIs

    How a small platform API resolves provider keys into a domain identity, separates tenant data, and chooses an authentication boundary that fits its scale.

  33. Where Cross-Domain Coordination Code Belongs

    Use business ownership, cohesion, imports, and a package-move experiment to place code that coordinates a primary action with another domain's rule.

  34. Do Not Create a DTO for Every Layer by Habit

    Use DTOs where data crosses a boundary with a different contract. Mapping has a cost, but so does letting external request shapes define the inside of a service.

  35. Put Circuit Breakers Next to the Failing I/O

    Circuit breakers, timeouts, cache fallbacks, and remote-call policies belong near the implementation that performs the I/O, while domain code chooses the required behavior.

  36. Do Not Split Modules Before the Domain Has Matured

    Domain maturity comes from understanding policy, behavior, and operating reality. Let those lessons reveal module boundaries instead of freezing guesses too early.

  37. Grow Software One Boundary at a Time

    Start with working code, then extract functions, classes, packages, modules, and projects as real cohesion and scale make each stronger boundary necessary.

  38. Keep Layers Logical and Packages Cohesive

    Modules, packages, and architectural layers solve different problems. Package related behavior together and let layers describe roles without scattering a feature.

  39. Software Should Survive the Developer Who Built It

    Good company software reduces debt, fits the team's operating ability, and remains understandable and repairable after its original developer leaves.

  40. Unit Tests Should Preserve Business Intent

    A business-layer unit test is useful when it guides design, records meaningful behavior, or makes the next developer reconsider a risky change.

  41. Multi-Module Too Early Makes Design Harder

    Modules should enforce boundaries discovered through implementation, not freeze guessed domains or mirror an architecture diagram before the software is understood.

  42. Gradle Dependency Scopes Are Architectural Boundaries

    Use Gradle implementation, api, runtimeOnly, and compileOnly deliberately to express module access, prevent accidental coupling, and preserve design intent.

  43. Isolate Admin Work from the Service Domain

    Admin APIs have different queries, mutation needs, and release risks. Isolate them by module or repository instead of letting operational convenience reshape the core service.

  44. Use Reader and Writer Components to Reveal Business Flow

    Reader and writer components can hide storage details, narrow change, and let the business layer show policy, but only when the software's lifetime justifies them.

  45. Reuse Below the Use-Case Layer

    When use cases call one another for reuse, business changes spread and cycles follow. Compose above them or extract focused implementation components below them.

  46. Choose Experience and Judgment over Development Jargon

    Theories and patterns are useful references, but a developer still needs to explain the code, its tradeoffs, and what happened when it was operated.

  47. How to Split a Large Service Class by Responsibility and Layer

    Use constructor dependencies and imports to diagnose an oversized service, then separate cohesive responsibilities before adding another architecture label.

  48. Choose Normalization from Requirements and Object Relationships

    Database normalization is not a score to maximize. Decide from change semantics, query cost, and the relationships the data is meant to preserve.