Introduction
This time, I'll cover "Map-Based Design," which is at the core of SIcore's Java code architecture.
As introduced in previous articles, SIcore uses "JSON-only communication." In the flow Browser → JSON → Java → JSON → Browser, we use the Map-based Io class as the Java-side data container. Since JSON is an associative array (key-value pairs), receiving it as a Map on the Java side is the most natural approach.
In SIcore, this Io class handles all requests, responses, and database operations, without using Entity (Bean) classes. This commitment enables reduced code volume, unified field naming, and strong compatibility with AI.
What This Article Covers
- What Map-based design is
- Why we use Map for everything
- Unified naming strategy
- Type-safe and null-safe data retrieval
- Bug prevention features
- Deep copy safety
- Benefits / Drawbacks
What is Map-Based Design?
On SIcore's server side (Java), all requests, responses, and database operations are handled with the Io class (a class that extends Map).
public void doExecute(final Io io) throws Exception {
// Retrieve values from request (key-based access)
String userId = io.getString("user_id");
BigDecimal incomeAm = io.getBigDecimal("income_am");
// Set database extraction result as response
IoItems row = SqlUtil.selectOne(getDbConn(), sb);
io.putAll(row);
// The io object becomes the response as-is
}
No Entity (Bean) classes are created. There's no need to prepare classes like UserEntity or OrderDto for each table or screen.
Why We Use Map for Everything
In SIcore, the entire process from browser to database uses Map (Io class).
[Browser] JSON → [Java] Io(Map) → [DB] SQL
← [Java] Io(Map) ←
Web Page Data is "Text"
Input values on web pages are all strings (String), even if they appear to be numbers or dates.
<!-- Appears as numbers or dates on the browser, but... -->
<input type="text" name="income_am" value="1200000">
<input type="text" name="birth_dt" value="20250101">
These remain strings throughout the journey JavaScript → JSON → Java. Type conversion is only needed at the moment you use them in business logic.
Also, numeric and date fields can have empty (blank) values. Storing as strings preserves empty values as-is, avoiding conversion errors that occur when Beans receive them as int or LocalDate.
The Io class stores everything internally as strings (String) and retrieves them with type conversion as needed.
// Retrieve as string
String incomeAmStr = io.getString("income_am"); // "1200000"
// Retrieve with type conversion when needed
BigDecimal incomeAm = io.getBigDecimal("income_am"); // 1200000
LocalDate birthDt = io.getDateNullable("birth_dt"); // 2025-01-01
What Increases When You Create Beans
Imagine a business system with 50 tables and 30 screens.
- Entity classes per table × 50
- Form / DTO classes per screen × 30 (or more)
- Entity ⇔ DTO conversion logic
- Camel case conversion (
user_id→userId) - Reflective getter / setter invocations
If you use Map for everything, all of these become unnecessary.
Same Approach as JavaScript
JSON, JavaScript associative arrays, and Java Maps all share the same structure: "access by key."
// JavaScript
const userId = req['user_id'];
// Java (Io class)
String userId = io.getString("user_id");
Data handling is unified from front to back.
Unified Naming Strategy
Database physical field name = HTML name attribute = Java Map key are unified.
-- Database
CREATE TABLE t_user (
user_id VARCHAR(10),
user_nm VARCHAR(50),
income_am NUMERIC(10),
birth_dt DATE
);
<!-- HTML -->
<input name="user_id">
<input name="user_nm">
<input name="income_am">
<input name="birth_dt">
// Java
String userId = io.getString("user_id");
String userNm = io.getString("user_nm");
Direct SQL Usage
Since field names are unified, Map data can be used directly in SQL.
public void doExecute(final Io io) throws Exception {
// io contents (request JSON as-is)
// {
// "user_id" : "U001",
// "user_nm" : "Mike Davis",
// "income_am" : "1200000",
// "birth_dt" : "20250101"
// }
SqlUtil.insertOne(getDbConn(), "t_user", io);
// ↑ Key names = DB field names, so it becomes an INSERT statement directly
// SQL executed:
// INSERT INTO t_user (user_id, user_nm, income_am, birth_dt)
// VALUES ('U001', 'Mike Davis', 1200000, 2025-01-01)
// ※SqlUtil automatically determines column types from DB metadata and binds with appropriate types
}
Database retrieval to response display works the same way. SELECT results can be used directly for screen display.
public void doExecute(final Io io) throws Exception {
// Retrieve extraction key from request
SqlBuilder sb = new SqlBuilder();
sb.addQuery("SELECT * FROM t_user WHERE user_id = ", io.getString("user_id")); // "user_id": "U001"
// Database extraction (result is IoItems = Map)
IoItems row = SqlUtil.selectOne(getDbConn(), sb);
// row contents:
// { "user_id": "U001", "user_nm": "Mike Davis", "income_am": "1200000", "birth_dt": "20250101" }
// Set to response → returns to browser as JSON as-is
io.putAll(row);
// ↑ Key names = HTML name attributes, so they're automatically set to each screen field
}
Benefits of Unified Naming:
-
No conversion code needed: No need for camel case conversion (
user_id→userId) - Reduced code volume: No need to write mapping logic
- Reduced bugs: No conversion mistakes
- Improved maintainability: Database design documents function as specifications
Type-Safe and Null-Safe Data Retrieval
You might be concerned: "Isn't it not type-safe without Beans?"
The Io class addresses this by providing type-safe and null-safe get methods.
Null Safety
In regular Maps, get() returns null, which causes NullPointerException.
// Regular Map
Map<String, String> map = new HashMap<>();
String value = map.get("key"); // null → causes NullPointerException
In the Io class, basic methods do not return null. To retrieve null, explicitly use Nullable methods.
// Io class
String value = io.getString("key"); // "" (blank instead of null)
String value = io.getStringNullable("key"); // null (explicitly retrieve null)
Type Safety
Type conversion methods are provided, and when type conversion errors occur, they log the key and value.
int age = io.getInt("age"); // Blank converts to zero
BigDecimal income = io.getBigDecimal("income_am"); // Preserves precision
LocalDate birthDt = io.getDateNullable("birth_dt"); // Date format check
Even when type conversion errors occur, the source is always consolidated in the Io class's get methods. Since the error log outputs the key and value, when giving correction instructions to AI, "which key's value is invalid" is clear, making validation and other countermeasures easy.
Bug Prevention Features
The Io class has features that prevent bugs commonly found in regular Maps.
Strict Key Duplication Check
Detects unintended value overwrites.
// Regular Map
map.put("user_id", "U001");
map.put("user_id", "U002"); // Overwritten (no warning)
// Io class
io.put("user_id", "U001");
io.put("user_id", "U002"); // Error (logs the key)
io.putForce("user_id", "U002"); // Use intentionally when overwriting
Non-Existent Key Retrieval Error
Detects typo mistakes.
// Regular Map
map.put("user_id", "U001");
String value = map.get("userid"); // null (doesn't notice typo)
// Io class
io.put("user_id", "U001");
io.getString("userid"); // Error (logs non-existent key)
These checks provide "declared variable"-like safety while being a Map.
Deep Copy Safety
The Io class performs deep copies when storing and retrieving lists and nested maps.
// Deep copy on storage
List<String> srcList = new ArrayList<>(Arrays.asList("A", "B"));
io.putList("items", srcList);
srcList.add("C"); // Modify original list
// io.getList("items") remains ["A", "B"] (no effect)
// Deep copy on retrieval
List<String> gotList = io.getList("items");
gotList.add("D"); // Modify retrieved list
// io.getList("items") remains ["A", "B"] (no effect)
This prevents unexpected side effects from reference sharing.
Benefits
- No Entity / DTO / Form classes needed: Code volume is significantly reduced
- No conversion code with unified naming: HTML → Java → SQL connects seamlessly
- Same approach as JavaScript: Unified key-based access from front to back
- Built-in bug prevention: Null-safe, type-safe, key duplication check, existence check
- Safe with deep copy: Prevents side effects from reference sharing
- Easy for AI to generate code: Patterns are simple and consistent
Drawbacks
-
IDE code completion doesn't work: Beans offer field name completion, but Map key strings don't get completed
- However, bug prevention features (error on non-existent key) cover this
- Also, AI generates code including key names, which covers this
-
No compile-time type checking: Beans guarantee field types at compile time, but Maps check at runtime
- However, Io class's type conversion methods safely convert at runtime
- Supplement with validation logic beforehand
Conclusion
Some may feel uneasy hearing "no Beans."
However, by eliminating the Entity / DTO / Form Bean class layer, code volume decreases, bugs from field name mismatches disappear, and work volume when adding new screens or tables is significantly reduced.
Combined with the Io class's bug prevention features (null-safe, type-safe, key duplication check, existence check), I believe the weaknesses of Map are practically covered.
Related Articles
Check out the other articles too!
- 001 Why I Built an SI Framework
- 002 Direct URL Mapping
- 003 JSON-Only Communication
- 004 Static Mockup = Implementation
- 005 Dynamic List Display
- 006 Custom HTML Attributes
- 007 Map-Based Design (this article)
- 008 Single-File CSS Design
SIcore Framework Links
All implementation code and documentation are available here:
- GitHub: https://github.com/sugaiketadao/sicore
- How to verify sample screens (VS Code): https://github.com/sugaiketadao/sicore#%EF%B8%8F-how-to-verify-sample-screens---vs-code
- Getting started with AI development: https://github.com/sugaiketadao/sicore#-getting-started-with-ai-development
Thank you for reading!
❤ Likes are very encouraging.
Top comments (0)