Why Architecture Matters in Mobile App Development
1. Why Architecture is Necessary 1-1. Favorable for Maintenance and Scalability As features increase, the codebase becomes more complex, making it difficult to identify where changes should be made. 1-2. Separation of Concerns Separating UI, business logic, and data handling allows each part to focus on its own responsibility, resulting in higher code quality. 1-3. Ease of Testing With clear layers, you can test core logic independently from the UI (unit testing). Unit testing allows developers to catch bugs early and confidently refactor or make changes. In a collaborative environment, running unit tests on PRs via CI (Continuous Integration) lets you instantly see if your changes break anything. Unit tests also act as living documentation, enabling faster debugging and maintenance. 1-4. Efficient Team Collaboration Clearly defined roles mean that multiple people can work simultaneously with fewer conflicts. 1-5. Resilient to Change Even if UI, network, or database implementations change, the core logic remains unaffected. 2. The Core of Architecture: Separation of Concerns & Business Logic Classification 2-1. What is Separation of Concerns? Separation of concerns means structuring your code so that UI, business logic, and data each have clear and distinct responsibilities. Among these, separating business logic from other code (like UI, networking, or data) is especially important. 2-2. What is Business Logic? Business logic refers to “what your app actually needs to do” — its core features, policies, rules, and calculations. Examples An Americano: Mix 1 shot of espresso with 150ml of water. A Latte: Mix 1 shot of espresso with 150ml of milk and add milk foam. For iced drinks: Always add ice. → All these “recipe rules” represent business logic. 2-3. Why Separate Business Logic? Maintenance & Scalability: Even if the UI or networking changes, the service policy (business logic) can remain unchanged. Ease of Testing: By isolating business logic, you can unit test it, catching bugs early and making refactoring less daunting. Collaboration: Multiple people can work at the same time with clear roles and fewer conflicts. Reusability: The same business logic can be reused across multiple screens or features. 3. When You're Unsure About Business Logic Boundaries When developing a feature, what planners or designers understand about how the service should behave is usually the business logic. In other words, “the rules or behaviors that must be preserved in this service” constitute business logic. View (UI): Only responsible for displaying the screen and handling input. Business Logic: Responsible for all other “functions, policies, conditions, calculations,” etc. 4. Example of Separating Business Logic (Coffee Making Example) Here's a simplified example without repositories or use cases. 4-1. Business Logic Mixed into the View (Bad Example) Android (Compose, Kotlin) @Composable fun CoffeeScreen() { var coffeeType by remember { mutableStateOf("") } var result by remember { mutableStateOf("") } Column { TextField( value = coffeeType, onValueChange = { coffeeType = it }, label = { Text("Enter coffee type (americano/latte)") } ) Button(onClick = { if (coffeeType == "americano") { result = "1 shot espresso + 150ml water" } else if (coffeeType == "latte") { result = "1 shot espresso + 150ml milk + milk foam" } else { result = "Unknown menu" } }) { Text("Make") } Text(result) } } iOS (SwiftUI, Swift) struct CoffeeScreen: View { @State private var coffeeType: String = "" @State private var result: String = "" var body: some View { VStack { TextField("Enter coffee type (americano/latte)", text: $coffeeType) .textFieldStyle(RoundedBorderTextFieldStyle()) Button("Make") { if coffeeType == "americano" { result = "1 shot espresso + 150ml water" } else if coffeeType == "latte" { result = "1 shot espresso + 150ml milk + milk foam" } else { result = "Unknown menu" } } Text(result) } .padding() } } Flutter (Dart) class CoffeeScreen extends StatefulWidget { @override State createState() => _CoffeeScreenState(); } class _CoffeeScreenState extends State { final coffeeTypeController = TextEditingController(); String resultText = ""; @override Widget build(BuildContext context) { return Column( children: [ TextField( contro

1. Why Architecture is Necessary
1-1. Favorable for Maintenance and Scalability
- As features increase, the codebase becomes more complex, making it difficult to identify where changes should be made.
1-2. Separation of Concerns
- Separating UI, business logic, and data handling allows each part to focus on its own responsibility, resulting in higher code quality.
1-3. Ease of Testing
- With clear layers, you can test core logic independently from the UI (unit testing).
- Unit testing allows developers to catch bugs early and confidently refactor or make changes.
- In a collaborative environment, running unit tests on PRs via CI (Continuous Integration) lets you instantly see if your changes break anything.
- Unit tests also act as living documentation, enabling faster debugging and maintenance.
1-4. Efficient Team Collaboration
- Clearly defined roles mean that multiple people can work simultaneously with fewer conflicts.
1-5. Resilient to Change
- Even if UI, network, or database implementations change, the core logic remains unaffected.
2. The Core of Architecture: Separation of Concerns & Business Logic Classification
2-1. What is Separation of Concerns?
- Separation of concerns means structuring your code so that UI, business logic, and data each have clear and distinct responsibilities.
- Among these, separating business logic from other code (like UI, networking, or data) is especially important.
2-2. What is Business Logic?
- Business logic refers to “what your app actually needs to do” — its core features, policies, rules, and calculations.
Examples
- An Americano: Mix 1 shot of espresso with 150ml of water.
- A Latte: Mix 1 shot of espresso with 150ml of milk and add milk foam.
- For iced drinks: Always add ice. → All these “recipe rules” represent business logic.
2-3. Why Separate Business Logic?
- Maintenance & Scalability: Even if the UI or networking changes, the service policy (business logic) can remain unchanged.
- Ease of Testing: By isolating business logic, you can unit test it, catching bugs early and making refactoring less daunting.
- Collaboration: Multiple people can work at the same time with clear roles and fewer conflicts.
- Reusability: The same business logic can be reused across multiple screens or features.
3. When You're Unsure About Business Logic Boundaries
When developing a feature, what planners or designers understand about how the service should behave is usually the business logic.
In other words, “the rules or behaviors that must be preserved in this service” constitute business logic.
- View (UI): Only responsible for displaying the screen and handling input.
- Business Logic: Responsible for all other “functions, policies, conditions, calculations,” etc.
4. Example of Separating Business Logic (Coffee Making Example)
Here's a simplified example without repositories or use cases.
4-1. Business Logic Mixed into the View (Bad Example)
Android (Compose, Kotlin)
@Composable
fun CoffeeScreen() {
var coffeeType by remember { mutableStateOf("") }
var result by remember { mutableStateOf("") }
Column {
TextField(
value = coffeeType,
onValueChange = { coffeeType = it },
label = { Text("Enter coffee type (americano/latte)") }
)
Button(onClick = {
if (coffeeType == "americano") {
result = "1 shot espresso + 150ml water"
} else if (coffeeType == "latte") {
result = "1 shot espresso + 150ml milk + milk foam"
} else {
result = "Unknown menu"
}
}) {
Text("Make")
}
Text(result)
}
}
iOS (SwiftUI, Swift)
struct CoffeeScreen: View {
@State private var coffeeType: String = ""
@State private var result: String = ""
var body: some View {
VStack {
TextField("Enter coffee type (americano/latte)", text: $coffeeType)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Make") {
if coffeeType == "americano" {
result = "1 shot espresso + 150ml water"
} else if coffeeType == "latte" {
result = "1 shot espresso + 150ml milk + milk foam"
} else {
result = "Unknown menu"
}
}
Text(result)
}
.padding()
}
}
Flutter (Dart)
class CoffeeScreen extends StatefulWidget {
@override
State<CoffeeScreen> createState() => _CoffeeScreenState();
}
class _CoffeeScreenState extends State<CoffeeScreen> {
final coffeeTypeController = TextEditingController();
String resultText = "";
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: coffeeTypeController,
decoration: InputDecoration(labelText: "Enter coffee type (americano/latte)"),
),
ElevatedButton(
onPressed: () {
String coffeeType = coffeeTypeController.text;
if (coffeeType == "americano") {
resultText = "1 shot espresso + 150ml water";
} else if (coffeeType == "latte") {
resultText = "1 shot espresso + 150ml milk + milk foam";
} else {
resultText = "Unknown menu";
}
setState(() {});
},
child: Text('Make'),
),
Text(resultText),
],
);
}
}
- The coffee recipe (business logic) is mixed into the view (UI) code.
4-2. Separating Business Logic into a ViewModel or Controller (Good Example)
Android (Compose, Kotlin)
// CoffeeViewModel.kt
class CoffeeViewModel : ViewModel() {
fun getRecipe(coffeeType: String): String {
return when (coffeeType) {
"americano" -> "1 shot espresso + 150ml water"
"latte" -> "1 shot espresso + 150ml milk + milk foam"
else -> "Unknown menu"
}
}
}
// CoffeeScreen.kt (View code)
@Composable
fun CoffeeScreen(viewModel: CoffeeViewModel = viewModel()) {
var coffeeType by remember { mutableStateOf("") }
var result by remember { mutableStateOf("") }
Column {
TextField(
value = coffeeType,
onValueChange = { coffeeType = it },
label = { Text("Enter coffee type (americano/latte)") }
)
Button(onClick = {
result = viewModel.getRecipe(coffeeType)
}) {
Text("Make")
}
Text(result)
}
}
iOS (SwiftUI, Swift)
// CoffeeViewModel.swift
class CoffeeViewModel: ObservableObject {
func getRecipe(coffeeType: String) -> String {
switch coffeeType {
case "americano":
return "1 shot espresso + 150ml water"
case "latte":
return "1 shot espresso + 150ml milk + milk foam"
default:
return "Unknown menu"
}
}
}
// CoffeeScreen.swift (View code)
struct CoffeeScreen: View {
@State private var coffeeType: String = ""
@State private var result: String = ""
@StateObject private var viewModel = CoffeeViewModel()
var body: some View {
VStack {
TextField("Enter coffee type (americano/latte)", text: $coffeeType)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Make") {
result = viewModel.getRecipe(coffeeType: coffeeType)
}
Text(result)
}
.padding()
}
}
Flutter (Dart)
// coffee_view_model.dart
class CoffeeViewModel {
String getRecipe(String coffeeType) {
switch (coffeeType) {
case "americano":
return "1 shot espresso + 150ml water";
case "latte":
return "1 shot espresso + 150ml milk + milk foam";
default:
return "Unknown menu";
}
}
}
// coffee_screen.dart (View code)
class CoffeeScreen extends StatefulWidget {
@override
State<CoffeeScreen> createState() => _CoffeeScreenState();
}
class _CoffeeScreenState extends State<CoffeeScreen> {
final coffeeTypeController = TextEditingController();
String resultText = "";
final coffeeViewModel = CoffeeViewModel();
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: coffeeTypeController,
decoration: InputDecoration(labelText: "Enter coffee type (americano/latte)"),
),
ElevatedButton(
onPressed: () {
resultText = coffeeViewModel.getRecipe(coffeeTypeController.text);
setState(() {});
},
child: Text('Make'),
),
Text(resultText),
],
);
}
}
- The coffee recipe (business logic) is separated out into the ViewModel/Controller.
- The view only handles input/output, while core policies are managed by the ViewModel/Controller.
- This makes testing, maintenance, and reusability much easier.
5. Benefits of Unit Testing for Developers
Being able to validate business logic with unit tests brings enormous advantages for developers.
5-1. Specific Advantages of Unit Testing
1. Catch Bugs Early
- Core logic can be tested independently of the UI or external environment, so even a single line change can be quickly validated to catch mistakes.
2. Fearless Refactoring
- With solid unit tests, you can refactor and improve your code confidently, knowing that any problems will be immediately evident from failing tests.
3. Easier Collaboration
- When multiple people are working on the same project, each person’s unit tests make it easy to see if changes affect other parts of the code.
- With unit tests running automatically in CI on every PR, you can instantly see if your changes cause any issues elsewhere.
4. Acts as Documentation
- Unit tests provide code examples showing how the logic is intended to work, helping other developers understand the core logic just by reading the tests.
5. Easier Debugging and Maintenance
- You can quickly and repeatedly run tests on specific business logic without launching the entire app, making it simple to find bugs or verify fixes.
5-2. Example: Unit Testing Only Business Logic
Android (Compose, Kotlin)
import org.junit.Assert.assertEquals
import org.junit.Test
class CoffeeViewModelTest {
private val viewModel = CoffeeViewModel()
@Test
fun `Americano recipe`() {
assertEquals("1 shot espresso + 150ml water", viewModel.getRecipe("americano"))
}
@Test
fun `Latte recipe`() {
assertEquals("1 shot espresso + 150ml milk + milk foam", viewModel.getRecipe("latte"))
}
@Test
fun `Unknown menu`() {
assertEquals("Unknown menu", viewModel.getRecipe("mocha"))
}
}
iOS (Swift)
import XCTest
class CoffeeViewModelTests: XCTestCase {
let viewModel = CoffeeViewModel()
func test_americano() {
XCTAssertEqual(viewModel.getRecipe(coffeeType: "americano"), "1 shot espresso + 150ml water")
}
func test_latte() {
XCTAssertEqual(viewModel.getRecipe(coffeeType: "latte"), "1 shot espresso + 150ml milk + milk foam")
}
func test_unknownMenu() {
XCTAssertEqual(viewModel.getRecipe(coffeeType: "mocha"), "Unknown menu")
}
}
Flutter (Dart)
import 'package:flutter_test/flutter_test.dart';
import 'coffee_view_model.dart';
void main() {
final viewModel = CoffeeViewModel();
test('Americano recipe', () {
expect(viewModel.getRecipe("americano"), "1 shot espresso + 150ml water");
});
test('Latte recipe', () {
expect(viewModel.getRecipe("latte"), "1 shot espresso + 150ml milk + milk foam");
});
test('Unknown menu', () {
expect(viewModel.getRecipe("mocha"), "Unknown menu");
});
}
- Business logic, separated from the UI, can be easily tested in isolation.
- These tests can be run automatically on every PR using CI.
6. Conclusion & Realistic Trade-offs
The main goal of architecture is to separate business logic from UI/infrastructure,
making code more adaptable to change, easier to maintain, collaborate on, and test (especially unit tests).
Realistic Trade-offs of Collaboration and Architecture
In real-world projects, multiple people work together, so consistent code style (syntax, naming, etc.) is important.
This is more about creating a maintainable and collaborative culture than a core aspect of architecture itself.-
By applying architecture:
- It’s clear which file to modify
- Mistakes and conflicts are reduced
- Maintenance is easier
-
On the other hand:
- More layers mean more files
- You may need to change several files for one feature
- There may be more repetitive (boilerplate) code
Conclusion
- Despite these inconveniences, in the long run, the clear separation of responsibilities, easier collaboration, maintainability, and improved quality far outweigh the downsides and make it well worth the effort.