DEV Community

sugaiketadao
sugaiketadao

Posted on • Edited on

Map-Based Design - I built a lightweight Java framework for Japan's "SI" projects (third attempt in 10 years) #007

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
}
Enter fullscreen mode Exit fullscreen mode

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)  ←
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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_iduserId)
  • 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'];
Enter fullscreen mode Exit fullscreen mode
// Java (Io class)
String userId = io.getString("user_id");
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode
<!-- HTML -->
<input name="user_id">
<input name="user_nm">
<input name="income_am">
<input name="birth_dt">
Enter fullscreen mode Exit fullscreen mode
// Java
String userId = io.getString("user_id");
String userNm = io.getString("user_nm");
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Benefits of Unified Naming:

  • No conversion code needed: No need for camel case conversion (user_iduserId)
  • 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!

SIcore Framework Links

All implementation code and documentation are available here:


Thank you for reading!
❤ Likes are very encouraging.

Top comments (0)