software-design

14 posts

line

Code Quality Improvement Techniques Part 29 (opens in new tab)

Complexity in software often arises from "Gordian Variables," where tangled data dependencies make the logic flow difficult to trace and maintain. By identifying and designing an ideal intermediate data structure, developers can decouple these dependencies and simplify complex operations. This approach replaces convoluted conditional checks with a clean, structured data flow that highlights the core business logic. ## The Complexity of Tangled Dependencies Synchronizing remote data with local storage often leads to fragmented logic when the relationship between data IDs and objects is not properly managed. * Initial implementations frequently use set operations like `subtract` on ID lists to determine which items to create, update, or delete. * This approach forces the program to re-access original data sets multiple times, creating a disconnected flow between identifying a change and executing it. * Dependency entanglements often necessitate "impossible" runtime error handling (e.g., `error("This must not happen")`) because the compiler cannot guarantee data presence within maps during the update phase. * Inconsistent processing patterns emerge, where "add" and "update" logic might follow one sequence while "delete" logic follows an entirely different one. ## Designing Around Intermediate Data Structures To untangle complex flows, developers should work backward from an ideal data representation that categorizes all possible states—additions, updates, and deletions. * The first step involves creating lookup maps for both remote and local entries to provide O(1) access to data objects. * A unified collection of all unique IDs from both sources serves as the foundation for a single, comprehensive transformation pass. * A specialized utility function, such as `partitionByNullity`, can transform a sequence of data pairs (`Pair<Remote?, Local?>`) into three distinct, non-nullable lists. * This transformation results in a `Triple` containing `createdEntries`, `updatedEntries` (as pairs), and `deletedEntries`, effectively separating data preparation from business execution. ## Improved Synchronization Flow Restructuring the function around categorized lists allows the primary synchronization logic to remain concise and readable. * The synchronization function becomes a sequence of two phases: data categorization followed by execution loops. * By using the `partitionByNullity` pattern, the code eliminates the need for manual null checks or "impossible" error branches during the update process. * The final implementation highlights the most important part of the code—the `forEach` blocks for adding, updating, and deleting—by removing the noise of ID-based lookups and set mathematics. When faced with complex data dependencies, prioritize the creation of a clean intermediate data structure over-optimizing individual logical branches. Designing a data flow that naturally represents the different states of your business logic will result in more robust, self-documenting, and maintainable code.

daangn

The Journey of Karrot Pay (opens in new tab)

Daangn Pay’s backend evolution demonstrates how software architecture must shift from a focus on development speed to a focus on long-term sustainability as a service grows. Over four years, the platform transitioned from a simple layered structure to a complex monorepo powered by Hexagonal and Clean Architecture principles to manage increasing domain complexity. This journey highlights that technical debt is often the price of early success, but structural refactoring is essential to support organizational scaling and maintain code quality. ## Early Speed with Layered Architecture * The initial system was built using a standard Controller-Service-Repository pattern to meet the urgent deadline for obtaining an electronic financial business license. * This simple structure allowed for rapid development and the successful launch of core remittance and wallet features. * As the service expanded to include promotions, billing, and points, the "Service" layer became overloaded with cross-cutting concerns like validation and permissions. * The lack of strict boundaries led to circular dependencies and "spaghetti code," making the system fragile and difficult to test or refactor. ## Decoupling Logic via Hexagonal Architecture * To address the tight coupling between business logic and infrastructure, the team adopted a Hexagonal (Ports and Adapters) approach. * The system was divided into three distinct modules: `domain` (pure POJO rules), `usecase` (orchestration of scenarios), and `adapter` (external implementations like DBs and APIs). * This separation ensured that core business logic remained independent of the Spring Framework or specific database technologies. * While this solved dependency issues and improved reusability across REST APIs and batch jobs, it introduced significant boilerplate code and the complexity of mapping between different data models (e.g., domain entities vs. persistence entities). ## Scaling to a Monorepo and Clean Architecture * As Daangn Pay grew from a single project into dozens of services handled by multiple teams, a Monorepo structure was implemented using Gradle multi-projects. * The architecture evolved to separate "Domain" modules (pure business logic) from "Service" modules (the actual runnable applications like API servers or workers). * An "Internal-First" policy was adopted, where modules are private by default and can only be accessed through explicitly defined public APIs to prevent accidental cross-domain contamination. * This setup currently manages over 30 services, providing a balance between code sharing and strict boundary enforcement between domains like Money, Billing, and Points. The evolution of Daangn Pay’s architecture serves as a practical reminder that there is no "perfect" architecture from the start; rather, the best design is one that adapts to the current size of the organization and the complexity of the business. Engineers should prioritize flexibility and structural constraints that guide developers toward correct patterns, ensuring the codebase remains manageable even as the team and service scale.

line

Code Quality Improvement Techniques Part (opens in new tab)

LY Corporation’s technical review highlights that making a class open for inheritance imposes a "tax" on its internal constraints, particularly immutability. While developers often use inheritance to create specialized versions of a class, doing so with immutable types can allow subclasses to inadvertently or intentionally break the parent class's guarantees. To ensure strict data integrity, the post concludes that classes intended to be immutable should be made final or designed around read-only interfaces rather than open for extension. ### The Risks of Open Immutable Classes * Kotlin developers often wrap `IntArray` in an `ImmutableIntList` to avoid the overhead of boxed types while ensuring the collection remains unchangeable. * If `ImmutableIntList` is marked as `open`, a developer might create a `MutableIntList` subclass that adds a `set` method to modify the internal `protected valueArray`, violating the "Immutable" contract of the parent type. * Even if the internal state is `private`, a subclass can override the `get` method to return dynamic or state-dependent values, effectively breaking the expectation that the data remains constant. * These issues demonstrate that any class with a "fundamental" name should be carefully guarded against unexpected inheritance in different modules or packages. ### Establishing Safe Inheritance Hierarchies * Mutable objects should not inherit from immutable objects, as this inherently violates the immutability constraints established by the parent. * Conversely, immutable objects should not inherit from mutable ones; this often leads to runtime errors (such as `UnsupportedOperationException`) when a user attempts to call modification methods like `add` or `set` on an immutable instance. * The most effective design pattern is to use a "read-only" (unmodifiable) interface as a common parent, similar to how Kotlin distinguishes between `List` and `MutableList`. * In this structure, mutable classes can inherit from the read-only parent without issue (adding new methods), and immutable classes can inherit from the read-only parent while adding stricter internal constraints. To maintain high code quality and prevent logic errors, developers should default to making classes final when immutability is a core requirement. If shared functionality is needed across different types of lists, utilize composition or a shared read-only interface to ensure that the "immutable" label remains a truthful guarantee.

line

Code Quality Improvement Techniques Part 27 (opens in new tab)

Over-engineering through excessive Dependency Injection (DI) can introduce unnecessary complexity and obscure a system's logic. While DI is a powerful tool for modularity, applying it to simple utility functions or data models often creates a maintenance burden without providing tangible benefits. Developers should aim to balance flexibility with simplicity by only injecting dependencies that serve a specific architectural purpose. ### The Risks of Excessive Dependency Injection Injecting every component, including simple formatters and model factories, can lead to several technical issues that degrade code maintainability: * **Obscured Logic Flow:** When utilities are hidden behind interfaces and injected via constructors, tracing the actual execution path requires navigating through multiple callers and implementation files, making the code harder to read. * **Increased Caller Responsibility:** Requiring dependencies for every small component forces the calling class to manage a "bloated" set of objects, often leading to a chain reaction where high-level classes must resolve dozens of unrelated dependencies. * **Data Inconsistency:** Injecting multiple utilities that rely on a shared state (like a `Locale`) creates a risk where a caller might accidentally pass mismatched configurations to different components, breaking the expected association between values. ### Valid Use Cases for Dependency Injection DI should be reserved for scenarios where the benefits of abstraction outweigh the cost of complexity. Proper use cases include: * **Lifecycle and Scope Management:** Sharing objects with specific lifecycles, such as those managing global state or cross-cutting concerns. * **Dependency Inversion:** Breaking circular dependencies between modules or ensuring the code adheres to specific architectural boundaries (e.g., Clean Architecture). * **Implementation Switching:** Enabling the replacement of components for different environments, such as swapping a real network repository for a mock implementation during unit testing or debugging. * **Decoupling for Build Performance:** Separating implementations into different modules to improve incremental build speeds or to isolate proprietary third-party libraries. ### Strategies for Refactoring and Simplification To improve code quality, developers should identify "transparent" dependencies that can be internalized or simplified: * **Direct Instantiation:** For simple data models like `NewsSnippet`, replace factory functions with direct constructor calls to clarify the intent and reduce boilerplate. * **Internalize Simple Utilities:** Classes like `TimeTextFormatter` or `StringTruncator` that perform basic logic can be maintained as private properties within the class or as stateless `object` singletons rather than being injected. * **Selective Injection:** Reserve constructor parameters for complex objects (e.g., repositories that handle network or database access) and environment-dependent values (e.g., a user's `Locale`). The core principle for maintaining a clean codebase is to ensure every injected dependency has a clear, documented purpose. By avoiding the trap of "injecting everything by default," developers can create systems that are easier to trace, test, and maintain.

woowahan

Considerations for Adopting Flutter into (opens in new tab)

To efficiently manage millions of daily orders across a diversifying device ecosystem including Windows, Android, macOS, and iOS, the Baedal Minjok Order Reception team adopted Flutter combined with Clean Architecture. This transition moved the team from redundant platform-specific development to a unified codebase approach that balances high development productivity with a consistent user experience. By focusing on "Write Once, Adapt Everywhere," the team successfully integrated complex platform-specific requirements while maintaining a scalable architectural foundation. ## Strategic Shift to Flutter and Multi-Platform Adaptation * **Business Efficiency**: Moving to a single codebase allowed the team to support Android, macOS, and Windows simultaneously, reducing the need for platform-specific developers and accelerating feature parity across devices. * **Adaptation over Portability**: The team shifted from the "Run Everywhere" ideal to "Adapt Everywhere," recognizing that different OSs require unique implementations for core features like app updates (Google Play In-App Updates for Android vs. Sparkle for macOS). * **Unified UX**: Providing a consistent interface across all devices lowered the learning curve for restaurant partners and reduced support issues arising from UI discrepancies between operating systems. ## Pragmatic Abstraction Strategy * **Abstraction Criteria**: To avoid over-engineering and excessive boilerplate, the team only applied abstractions when implementations varied by platform, relied on external libraries prone to change, or required mocking for tests. * **Infrastructure Isolation**: Technical implementations like `AppUpdateManager` and `LocalNotification` were hidden behind interfaces, allowing the business logic to remain independent of the underlying technology. * **Case Study (MQTT to SSE)**: Because real-time communication was abstracted via a `ServerEventReceiver` interface, the team successfully transitioned from MQTT to Server-Sent Events (SSE) by simply swapping the implementation class without modifying any business logic. ## Clean Architecture and BLoC Implementation * **Layered Design**: The project follows a strict separation into Data (Repository Impl, DTO), Domain (Entity, UseCase, Interfaces), and Presentation (UI, BLoC) layers, with an additional Infrastructure layer for hardware-specific tasks like printing. * **Explicit State Management**: The BLoC (Business Logic Component) pattern was chosen for its stream-based approach, which provides a clear audit trail of events and states (e.g., tracking an order list from `InitializeListEvent` to `LoadedOrderListState`). * **Reliability over Conciseness**: Despite the boilerplate code required by BLoC, the team prioritized the ability to trace state changes and debug complex business flows in a high-traffic production environment. ## Evolution Toward an App Shell Model * **Rapid Deployment**: To further increase agility, the team is transitioning toward a WebView-based "App Shell" container, which allows for immediate web-based feature updates that bypass lengthy app store review processes. * **Hybrid Approach**: While the core "Shell" remains in Flutter to handle system-level permissions and hardware integration, the business features are increasingly delivered via web technologies to maintain high update frequency. By establishing a robust foundation with Flutter and Clean Architecture, the team has successfully balanced the need for cross-platform development speed with the technical rigor required for a mission-critical order reception system. Their pragmatic approach to abstraction ensures the system remains maintainable even as underlying communication protocols or platform requirements evolve.

line

Code Quality Improvement Techniques Part 24: The Value of Legacy (opens in new tab)

The LY Corporation Review Committee advocates for simplifying code by avoiding unnecessary inheritance when differences between classes are limited to static data rather than dynamic logic. By replacing complex interfaces and subclasses with simple data models and specific instances, developers can reduce architectural overhead and improve code readability. This approach ensures that configurations, such as UI themes, remain predictable and easier to maintain without the baggage of a type hierarchy. ### Limitations of Inheritance-Based Configuration * The initial implementation used a `FooScreenThemeStrategy` interface to define UI elements like background colors, text colors, and icons. * Specific themes (Light and Dark) were implemented as separate classes that overridden the interface properties. * This pattern creates an unnecessary proliferation of types when the only difference between the themes is the specific value of the constants being returned. * Using inheritance for simple value changes makes the code harder to follow and can lead to over-engineering. ### Valid Scenarios for Inheritance * **Dynamic Logic:** When behavior needs to change dynamically at runtime via dynamic dispatch. * **Sum Types:** Implementing restricted class hierarchies, such as Kotlin `sealed` classes or Java's equivalent. * **Decoupling:** Separating interface from implementation to satisfy DI container requirements or to improve build speeds. * **Dependency Inversion:** Applying architectural patterns to resolve circular dependencies or to enforce one-way dependency flows. ### Transitioning to Data Models and Instantiation * Instead of an interface, a single "final" class or data class (e.g., `FooScreenThemeModel`) should be defined to hold the required properties. * Individual themes are created as simple instances of this model rather than unique subclasses. * In Kotlin, defining a class without the `open` keyword ensures that the properties are not dynamically altered and that no hidden, instance-specific logic is introduced. * This "instantiation over inheritance" strategy guarantees that properties remain static and the code remains concise. To maintain a clean codebase, prioritize data-driven instantiation over class-based inheritance whenever logic remains constant. This practice reduces the complexity of the type system and makes the code more resilient to unintended side effects.

line

Code Quality Improvement Techniques Part 22 (opens in new tab)

The post argues that developers should avoid overriding the `equals` method to compare only a subset of an object’s properties, as this violates the fundamental principles of identity and structural equivalence. Implementing "partial equality" often leads to subtle, hard-to-trace bugs in reactive programming environments where UI updates depend on detecting changes through equality checks. To ensure system reliability, `equals` must strictly represent either referential identity or total structural equivalence. ### Risks of Partial Equality in Reactive UI * Reactive frameworks such as Kotlin’s `StateFlow`, `Flow`, and Android’s `LiveData` utilize `distinctUntilChanged` logic to optimize performance. * These "observable" patterns compare the new object instance with the previous one using `equals`; if the result is `true`, the update is ignored to prevent unnecessary re-rendering. * If a `UserProfileViewData` object only compares a `userId` field, the UI will fail to reflect changes to a user's nickname or profile image because the framework incorrectly assumes the data has not changed. * To avoid this, any comparison logic that only checks specific fields should be moved to a uniquely named function, such as `hasSameIdWith()`, instead of hijacking the standard `equals` method. ### Defining Identity vs. Equivalence * **Identity (Referential Equality):** This indicates that two references point to the exact same object instance, which is the default behavior of `Object.equals()` in Java or `Any.equals()` in Kotlin. * **Equivalence (Structural Equality):** This indicates that two objects are logically the same because all their properties match. In Kotlin, `data class` implementations provide this by default for all parameters defined in the primary constructor. * Proper implementation of equivalence requires that all fields within the object also have clearly defined equality logic. ### Nuances and Implementation Exceptions * **Kotlin Data Class Limitations:** Only properties declared in the primary constructor are included in the compiler-generated `equals` and `hashCode` methods; properties declared in the class body are ignored by default. * **Calculated Caches:** It is acceptable to exclude certain fields from an equality check if they do not change the logical state of the object, such as a `cachedValue` used to store the results of a heavy mathematical operation. * **Context-Dependent Equality:** The definition of equality can change based on the model's purpose. For example, a mathematical model might treat 1/2 and 2/4 as equal, whereas a UI display model might treat them as different because they represent different strings of text. When implementing `equals`, prioritize full structural equivalence to prevent data-stale bugs in reactive systems. If you only need to compare a unique identifier, create a dedicated method instead of repurposing the standard equality check.

line

Code Quality Improvement Techniques Part 19: Child Lock (opens in new tab)

The "child lock" technique focuses on improving code robustness by restricting the scope of what child classes can override in an inheritance hierarchy. By moving away from broad, overridable functions that rely on manual `super` calls, developers can prevent common implementation errors and ensure that core logic remains intact across all subclasses. This approach shifts the responsibility of maintaining the execution flow to the parent class, making the codebase more predictable and easier to maintain. ## Problems with Open Functions and Manual Super Calls Providing an `open` function in a parent class that requires child classes to call `super` creates several risks: * **Missing `super` calls:** If a developer forgets to call `super.bind()`, the essential logic in the parent class (such as updating headers or footers) is skipped, often leading to silent bugs that are difficult to track. * **Implicit requirements:** Relying on inline comments to tell developers they must override a function is brittle. If the method isn't `abstract`, the compiler cannot enforce that the child class implements necessary logic. * **Mismatched responsibilities:** When a single function handles both shared logic and specific implementations, the responsibility of the code becomes blurred, making it easier for child classes to introduce side effects or incorrect behavior. ## Implementing the "Child Lock" with Template Methods To resolve these issues, the post recommends a pattern often referred to as the Template Method pattern: * **Seal the execution flow:** Remove the `open` modifier from the primary entry point (e.g., the `bind` method). This prevents child classes from changing the overall sequence of operations. * **Separate concerns:** Move the customizable portion of the logic into a new `protected abstract` function. * **Enforced implementation:** Because the new function is `abstract`, the compiler forces every child class to provide an implementation, ensuring that specific logic is never accidentally omitted. * **Guaranteed execution:** The parent class calls the abstract method from within its non-overridable method, ensuring that shared logic (like UI updates) always runs regardless of how the child is implemented. ## Refining Overridability and Language Considerations Designing for inheritance requires careful control over how child classes interact with parent logic: * **Avoid "super" dependency:** Generally, if a child class must explicitly call a parent function to work correctly, the inheritance structure is too loose. Exceptions are usually limited to lifecycle methods like `onCreate` in Android or constructors/destructors. * **C++ Private Virtuals:** In C++, developers can use `private virtual` functions. These allow a parent class to define a rigid flow in a public method while still allowing subclasses to provide specific implementations for the private virtual components, even though the child cannot call those functions directly. To ensure long-term code quality, the range of overridability should be limited as much as possible. By narrowing the interface between parent and child classes, you create a more rigid "contract" that prevents accidental bugs and clarifies the intent of the code.

line

Introducing a case study of (opens in new tab)

LY Corporation’s ABC Studio developed a specialized retail Merchant system by leveraging Domain-Driven Design (DDD) to overcome the functional limitations of a legacy food-delivery infrastructure. The project demonstrates that the primary value of DDD lies not just in technical implementation, but in aligning organizational structures and team responsibilities with domain boundaries. By focusing on the roles and responsibilities of the system rather than just the code, the team created a scalable platform capable of supporting diverse consumer interfaces. ### Redefining the Retail Domain * The legacy system treated retail items like restaurant entries, creating friction for specialized retail services; the new system was built to be a standalone platform. * The team narrowed the domain focus to five core areas: Shop, Item, Category, Inventory, and Order. * Sales-specific logic, such as coupons and promotions, was delegated to external "Consumer Platforms," allowing the Merchant system to serve as a high-performance information provider. ### Clean Architecture and Modular Composition * The system utilizes Clean Architecture to ensure domain entities remain independent of external frameworks, which also provided a manageable learning curve for new team members. * Services are split into two distinct modules: "API" modules for receiving external requests and "Engine" modules for processing business logic. * Communication between these modules is handled asynchronously via gRPC and Apache Kafka, using the Decaton library to increase throughput while maintaining a low partition count. * The architecture prioritizes eventual consistency, allowing for high responsiveness and scalability across the platform. ### Global Collaboration and Conway’s Law * Development was split between teams in Korea (Core Domain) and Japan (System Integration and BFF), requiring a shared understanding of domain boundaries. * Architectural Decision Records (ADR) were implemented to document critical decisions and prevent "knowledge drift" during long-term collaboration. * The organizational structure was intentionally designed to mirror the system architecture, with specific teams (Core, Link, BFF, and Merchant Link) assigned to distinct domain layers. * This alignment, reflecting Conway’s Law, ensures that changes to external consumer platforms have minimal impact on the stable core domain logic. Successful DDD adoption requires moving beyond technical patterns like hexagonal architecture and focusing on establishing a shared understanding of roles across the organization. By structuring teams to match domain boundaries, companies can build resilient systems where the core business logic remains protected even as the external service ecosystem evolves.

line

Code Quality Improvement Techniques Part 14 (opens in new tab)

Applying the Single Responsibility Principle is a fundamental practice for maintaining high code quality, but over-fragmenting logic can inadvertently lead to architectural complexity. While splitting classes aims to increase cohesion, it can also scatter business constraints and force callers to manage an overwhelming number of dependencies. This post explores the "responsibility of assigning responsibility," arguing that sometimes maintaining a slightly larger, consolidated class is preferable to creating fragmented "Ravioli code." ### Initial Implementation and the Refactoring Drive The scenario involves a dynamic "Launch Button" that can fire rockets, fireworks, or products depending on its mode. * The initial design used a single `LaunchButtonBinder` that held references to all possible `Launcher` types and an internal enum to select the active one. * To strictly follow the Single Responsibility Principle, developers often attempt to split this into two parts: a binder for the button logic and a selector for choosing the mode. * The refactored approach utilized a `LaunchBinderSelector` to manage multiple `LaunchButtonBinder` instances, using an `isEnabled` flag to toggle which logic was active. ### The Problem of Scattered Constraints and State While the refactored classes are individually simpler, the overall system becomes harder to reason about due to fragmented logic. * **Verification Difficulty:** In the original code, the constraint that "only one thing launches at a time" was obvious in a single file; in the refactored version, a developer must trace multiple classes and loops to verify this behavior. * **State Redundancy:** Adding an `isEnabled` property to binders creates a risk of state synchronization issues between the selector’s current mode and the binders' internal flags. * **Information Hiding Trade-offs:** Attempting to hide implementation details often forces the caller to resolve all dependencies (binders, buttons, and launchers) manually, which can turn the caller into a bloated "God class." ### Avoiding "Ravioli Code" Through Balanced Design The pursuit of granular responsibilities can lead to "Ravioli code," where the system consists of many small, independent components but lacks a clear, cohesive structure. * The original implementation’s advantage was that it encapsulated all logic related to the launch button's constraints in one place. * When deciding to split a class, developers must evaluate if the move improves the overall system or simply shifts the burden of complexity to the caller. * Effective design requires balancing individual class cohesion with the overhead of inter-module coupling and dependency management. When refactoring for code quality, prioritize the clarity of the overall system over the dogmatic pursuit of small classes. If splitting a class makes it harder to verify business constraints or complicates the caller's logic significantly, it may be better to keep those related responsibilities together.

line

Code Quality Improvement Techniques Part 1 (opens in new tab)

The "Set Discount" technique improves code quality by grouping related mutable properties into a single state object rather than allowing them to be updated individually. By restricting state changes through a controlled interface, developers can prevent inconsistent configurations and simplify the lifecycle management of complex classes. This approach ensures that dependent values are updated atomically, significantly reducing bugs caused by race conditions or stale data. ### The Risks of Fragmented Mutability When a class exposes multiple independent mutable properties, such as `isActive`, `minImportanceToRecord`, and `dataCountPerSampling`, it creates several maintenance challenges: * **Order Dependency:** Developers might accidentally set `isActive` to true before updating the configuration properties, causing the system to briefly run with stale or incorrect settings. * **Inconsistent Logic:** Internal state resets (like clearing a counter) may be tied to one property but forgotten when another related property changes, leading to unpredictable behavior. * **Concurrency Issues:** Even in single-threaded environments, asynchronous updates to individual properties can create race conditions that are difficult to debug. ### Consolidating State with SamplingPolicy To resolve these issues, the post recommends refactoring individual properties into a dedicated configuration class and using a single reference to manage the state: * **Atomic Updates:** By wrapping configuration values into a `SamplingPolicy` class, the system ensures that the minimum importance level and sampling interval are always updated together. * **Representing "Inactive" with Nulls:** Instead of a separate boolean flag, the `policy` property can be made nullable. An `inactive` state is naturally represented by `null`, making it impossible to "activate" the recorder without providing a valid policy. * **Explicit Lifecycle Methods:** Replacing property setters with methods like `startRecording()` and `finishRecording()` forces a clear transition of state and ensures that counters are reset consistently every time a new session begins. ### Advantages of Restricting State Transitions Moving from individual property mutation to a consolidated interface offers several technical benefits: * **Guaranteed Consistency:** It eliminates the possibility of "half-configured" states because the policy is replaced as a whole. * **Simplified Thread Safety:** If the class needs to be thread-safe, developers only need to synchronize a single reference update rather than coordinating multiple volatile variables. * **Improved Readability:** The intent of the code becomes clearer to future maintainers because the valid combinations of state are explicitly defined by the API. When designing components where properties are interdependent or must change simultaneously, you should avoid providing public setters for every field. Instead, provide a focused interface that limits updates to valid combinations, ensuring the object remains in a predictable state throughout its lifecycle.

line

Code Quality Improvement Techniques Part 1 (opens in new tab)

Effective code design often involves shifting the responsibility of state verification from the caller to the receiving object. By internalizing "if-checks" within the function that performs the action, developers can reduce boilerplate, prevent bugs caused by missing preconditions, and simplify state transitions. This encapsulation ensures that objects maintain their own integrity while providing a cleaner, more intuitive API for the rest of the system. ### Internalizing State Verification * Instead of the caller using a pattern like `if (!receiver.isState()) { receiver.doAction() }`, the check should be moved inside the `doAction` method. * Moving the check inside the function prevents bugs that occur when a caller forgets to verify the state, which could otherwise lead to crashes or invalid data transitions. * This approach hides internal state details from the caller, simplifying the object's interface and focusing on the desired outcome rather than the prerequisite checks. * If "doing nothing" when a condition isn't met is non-obvious, developers should use descriptive naming (e.g., `markAsFriendIfNotYet`) or clear documentation to signal this behavior. ### Leveraging Return Values for Conditional Logic * When a caller needs to trigger a secondary effect—such as showing a UI popup—only if an action was successful, it is better to return a status value (like a `Boolean`) rather than using higher-order functions. * Passing callbacks like `onSucceeded` into a use case can create unnecessary dependency cycles and makes it difficult for the caller to discern if the execution is synchronous or asynchronous. * Returning a `Boolean` to indicate if a state change actually occurred allows the caller to handle side effects cleanly and sequentially. * To ensure the caller doesn't ignore these results, developers can use documentation or specific compiler annotations to force the verification of the returned value. To improve overall code quality, prioritize "telling" an object what to do rather than "asking" about its state and then acting. Centralizing state logic within the receiver not only makes the code more robust against future changes but also makes the intent of the calling code much easier to follow.

line

Code Quality Improvement Techniques Part 1 (opens in new tab)

Maintaining a clear separation of concerns between software layers requires avoiding implicit dependencies where one layer relies on the specific implementation details of another. When different components share "hidden" knowledge—such as a repository fetching extra data specifically to trigger a UI state—the code becomes fragile and difficult to maintain. By passing explicit information through data models, developers can decouple these layers and ensure that changes in one do not inadvertently break the other. ### The Risks of Implicit Layer Dependency When layers share implicit logic, such as a repository layer knowing the specific display requirements of the UI, the architecture becomes tightly coupled and prone to bugs. * In the initial example, the repository fetches `MAX + 1` items specifically because the UI needs to display a "+" sign if more items exist. * This creates a dependency where the UI logic for displaying counts relies entirely on the repository's internal fetching behavior. * Code comments that explain one layer's behavior in the context of another (e.g., `// +1 is for the UI`) are a "code smell" indicating that responsibilities are poorly defined. ### Decoupling Through Explicit State The most effective way to separate these concerns is to modify the data model to carry explicit state information, removing the need for "magic numbers" or leaked logic. * By adding a boolean property like `hasMoreItems` to the `StoredItems` model, the repository can explicitly communicate the existence of additional data. * The repository handles the logic of fetching `limit + 1`, determining the boolean state, and then truncating the list to the correct size before passing it up. * The UI layer becomes "dumb" and only reacts to the provided data; it no longer needs to know about the `MAX_COUNT` constant or the repository's fetching strategy to determine its display state. ### Strategic Placement of Logic and Constants Determining where constants like `ITEM_LIST_MAX_COUNT` should reside is a key architectural decision that impacts code reuse and clarity. * **Business Logic Layer:** Placing such constants in a dedicated Domain or Use Case layer is often the best approach for maintaining a clean architecture. * **Model Classes:** If a separate logic layer is too complex for the project scale, the constant can be housed within the model class (e.g., using a companion object in Kotlin). * **Dependency Direction:** Developers must ensure that functional logic does not leak into generic data models, as this can create confusing dependencies where a general-purpose model becomes tied to a specific feature's algorithm. Effective software design relies on components maintaining a "proper distance" from one another. To improve code quality, favor explicit flags and clear data contracts over implicit assumptions about how different layers of the stack will interact.