I recently applied for a Java developer position and I was asked to completed a coding task.
After review of the coding task, the company said they do not want to take me forward to interview. They refused to give feedback so I have no idea of what went wrong.
I'd love some insights on what I did wrong.
The task:
You have been tasked with building part of a simple online auction system which will allow users to bid on items for sale.
Provide a bid tracker interface and concrete implementation with the following functionality:
- Record a user's bid on an item
- Get the current winning bid for an item
- Get all the bids for an item
- Get all the items on which a user has bid
You are not required to implement a GUI (or CLI) or persistent storage. You may use any appropriate libraries to help, but do not include the jars or class files in your submission.
The code:
The interface:
package com.nbentayou.app.service;
import com.nbentayou.app.exception.InvalidBidException;
import com.nbentayou.app.model.Bid;
import com.nbentayou.app.model.Item;
import com.nbentayou.app.model.User;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* The interface for a Bid Tracker.
* This interface exposes methods allowing {@link User}s to post {@link Bid}s on {@link Item}s
* and query the current state of the auction.
*/
public interface BidTracker {
/**
* Records a bid for a given item.
* @param bid the bid to record
* @param item the item to bid on
* @throws InvalidBidException when the bid is invalid
*/
void recordUserBidOnItem(Bid bid, Item item) throws InvalidBidException;
/**
* @param item the item
* @return the current winning bid (last valid bid), as an {@link Optional}, for the given item
*/
Optional<Bid> getCurrentWinningBidForItem(Item item);
/**
* @param item the item
* @return the list of all bids made for the given item
*/
List<Bid> getAllBidsForItem(Item item);
/**
* @param user the user to get the list of items for
* @return the list of all items bid on by the given user
*/
Set<Item> getAllItemsWithBidFromUser(User user);
}
The implementation
package com.nbentayou.app.service;
import com.nbentayou.app.exception.InvalidBidException;
import com.nbentayou.app.model.Bid;
import com.nbentayou.app.model.Item;
import com.nbentayou.app.model.User;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* An implementation of the {@link BidTracker} interface with an in-memory data storage.
*
* Some limitations of the current implementation are:
* - It stores the data about the auction in a ConcurrentHashMap. Other implementations could query a persistent
* data storage (Database, flat files, ..)
* - It assumes all the Users given as parameters are allowed to place a bid.
* - It assumes all the Items given as parameters can be bid on.
*/
public class BidTrackerImpl implements BidTracker {
private final Map<Item, List<Bid>> auctionBoard;
public BidTrackerImpl() {
auctionBoard = new ConcurrentHashMap<>();
}
/**
* Getter for the auction board.
* It returns a copy of the auction board to ensure that no external object can modify it.
* @return a copy of the auctionBoard
*/
public Map<Item, List<Bid>> getCurrentAuctionBoardCopy() {
return new HashMap<>(auctionBoard);
}
@Override
public void recordUserBidOnItem(Bid bid, Item item) throws InvalidBidException {
checkForNull(bid);
checkForNull(item);
recordUserBidOnItemSync(bid,item);
}
// synchronized method ensuring that only one bid is processed at a time.
private synchronized void recordUserBidOnItemSync(Bid bid, Item item) throws InvalidBidException {
checkBidIsHighEnough(bid, item);
addBidOnItem(item, bid);
}
private void checkBidIsHighEnough(Bid bid, Item item) throws InvalidBidException {
Optional<Bid> currentWinningBid = getCurrentWinningBidForItem(item);
if(currentWinningBid.isPresent() && bid.getValue() <= currentWinningBid.get().getValue()) {
throw new InvalidBidException(String.format(
"A bid of £%s on item %s is too low. It should be more than the current winning bid: £%s)",
bid.getValue(),
item,
currentWinningBid.get().getValue()));
}
}
@Override
public Optional<Bid> getCurrentWinningBidForItem(Item item) {
LinkedList<Bid> bids = new LinkedList<>(getAllBidsForItem(item));
return bids.isEmpty() ? Optional.empty() : Optional.of(bids.getLast());
}
@Override
public List<Bid> getAllBidsForItem(Item item) {
checkForNull(item);
return auctionBoard.getOrDefault(item, new ArrayList<>());
}
@Override
public Set<Item> getAllItemsWithBidFromUser(User user) {
return auctionBoard.entrySet().stream()
.filter(entry -> containsBidFromUser(entry.getValue(), user))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
/* Utility methods */
private boolean containsBidFromUser(List<Bid> bidsList, User user) {
return bidsList.stream().anyMatch(bid -> bid.isFromUser(user));
}
private void addBidOnItem(Item item, Bid bid) {
List<Bid> bidsOnItem = auctionBoard.getOrDefault(item, new ArrayList<>());
bidsOnItem.add(bid);
auctionBoard.put(item, bidsOnItem);
}
private void checkForNull(Item item) {
if(item == null)
throw new IllegalArgumentException("Item can't be null");
}
private void checkForNull(Bid bid) {
if(bid == null)
throw new IllegalArgumentException("Bid can't be null");
if(bid.getUser() == null)
throw new IllegalArgumentException("Bid's user can't be null");
}
}
Bid POJO
package com.nbentayou.app.model;
import java.util.Objects;
/**
* The representation of an auction bid from a user
*/
public class Bid {
private final User user;
private final int value;
public Bid(User user, int value) {
this.user = user;
this.value = value;
}
public User getUser() {
return user;
}
public int getValue() {
return value;
}
public boolean isFromUser(User user) {
return this.user.equals(user);
}
@Override
public String toString() {
return String.format("{ user: %s, value: %s }", user, value);
}
@Override
public boolean equals(Object o) {
boolean equals = false;
if (o == this) {
equals = true;
} else if (o instanceof Bid) {
Bid bid = (Bid) o;
equals = Objects.equals(user, bid.user) && value == bid.value;
}
return equals;
}
@Override
public int hashCode() {
return Objects.hash(user, value);
}
}
Item POJO
package com.nbentayou.app.model;
import java.util.Objects;
/**
* The representation of an item of the auction
*/
public class Item {
private final String id;
private final String name;
private final String description;
public Item(String id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
@Override
public String toString() {
return String.format("{ id: %s, name: %s, description: %s }", id, name, description);
}
@Override
public boolean equals(Object o) {
boolean equals = false;
if (o == this) {
equals = true;
} else if (o instanceof Item) {
Item item = (Item) o;
equals = Objects.equals(id, item.id) && Objects.equals(name, item.name) && Objects.equals(description, item.description);
}
return equals;
}
@Override
public int hashCode() {
return Objects.hash(id, name, description);
}
}
User POJO
package com.nbentayou.app.model;
import java.util.Objects;
/**
* The representation of a user of the auction
*/
public class User {
private final String id;
private final String name;
public User(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return String.format("{ id: %s, name: %s }", id, name);
}
@Override
public boolean equals(Object o) {
boolean equals = false;
if (o == this) {
equals = true;
} else if (o instanceof User) {
User user = (User) o;
equals = Objects.equals(id, user.id) && Objects.equals(name, user.name);
}
return equals;
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
InvalidBidException
package com.nbentayou.app.exception;
/**
* A Generic {@link Exception} thrown when trying to make an invalid bid.
* Could be for multiple reasons: The bid is too low, the item is not in the auction anymore, etc..
* The reason should be explained in the message or this Exception could be subclassed by finer grain Exceptions.
*/
public class InvalidBidException extends Exception {
public InvalidBidException(String message) {
super(message);
}
}
Unit tests
package com.nbentayou.app.service;
import com.nbentayou.app.exception.InvalidBidException;
import com.nbentayou.app.model.Bid;
import com.nbentayou.app.model.Item;
import com.nbentayou.app.model.User;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.junit.Assert.*;
public class BidTrackerImplTest {
private final User user1 = new User("u1", "Nicolas Bentayou");
private final User user2 = new User("u2", "Randolph Carter");
private final User user3 = new User("u3", "Herbert West");
private final Item item1 = new Item("i1", "item1", "Brilliant!");
private final Item item2 = new Item("i2", "item2", "Brilliant!");
private final Item item3 = new Item("i3", "item3", "Brilliant!");
private BidTrackerImpl bidTracker;
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Before
public void initAuctionRoom() {
bidTracker = new BidTrackerImpl();
}
@Test
public void recordUserBidOnItem_shouldAddAFirstBidToItem_whenBidIsValid() throws InvalidBidException {
Bid bid = new Bid(user1, 10);
bidTracker.recordUserBidOnItem(bid, item1);
List<Bid> actualBidListOnItem1 = bidTracker.getCurrentAuctionBoardCopy().get(item1);
List<Bid> expectedBidListOnItem1 = Collections.singletonList(bid);
assertEquals(expectedBidListOnItem1, actualBidListOnItem1);
}
@Test
public void recordUserBidOnItem_shouldAddSeveralBidsToItem_whenBidsAreValid() throws InvalidBidException {
bidTracker.recordUserBidOnItem(new Bid(user1, 10), item1);
bidTracker.recordUserBidOnItem(new Bid(user2, 20), item1);
bidTracker.recordUserBidOnItem(new Bid(user1, 30), item1);
List<Bid> actualBidsListOnItem1 = bidTracker.getCurrentAuctionBoardCopy().get(item1);
List<Bid> expectedBidsListOnItem1 = Arrays.asList(
new Bid(user1, 10),
new Bid(user2, 20),
new Bid(user1, 30));
assertEquals(expectedBidsListOnItem1, actualBidsListOnItem1);
}
@Test
public void recordUserBidOnItem_shouldThrowIllegalArgumentException_whenItemIsNull() throws InvalidBidException {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Item can't be null");
bidTracker.recordUserBidOnItem(new Bid(user1, 10), null);
}
@Test
public void recordUserBidOnItem_shouldThrowIllegalArgumentException_whenBidIsNull() throws InvalidBidException {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Bid can't be null");
bidTracker.recordUserBidOnItem(null, item1);
}
@Test
public void recordUserBidOnItem_shouldThrowIllegalArgumentException_whenUserIsNull() throws InvalidBidException {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Bid's user can't be null");
bidTracker.recordUserBidOnItem(new Bid(null, 10), item1);
}
@Test
public void recordUserBidOnItem_shouldThrowInvalidBidException_whenBidIsLowerThanCurrentlyWinningBid() throws InvalidBidException {
thrown.expect(InvalidBidException.class);
thrown.expectMessage("A bid of £5 on item { id: i1, name: item1, description: Brilliant! } is too low. It should be more than the current winning bid: £10)");
bidTracker.recordUserBidOnItem(new Bid(user1, 10), item1);
Bid lowBid = new Bid(user2, 5);
bidTracker.recordUserBidOnItem(lowBid, item1);
}
@Test
public void recordUserBidOnItem_shouldThrowInvalidBidException_whenBidIsSameAsCurrentlyWinningBid() throws InvalidBidException {
thrown.expect(InvalidBidException.class);
thrown.expectMessage("A bid of £10 on item { id: i1, name: item1, description: Brilliant! } is too low. It should be more than the current winning bid: £10)");
bidTracker.recordUserBidOnItem(new Bid(user1, 10), item1);
Bid sameBid = new Bid(user2, 10);
bidTracker.recordUserBidOnItem(sameBid, item1);
}
@Test
public void recordUserBidOnItem_shouldAddSeveralBidsToItem_whenSomeBidsAreInvalid() throws InvalidBidException {
bidTracker.recordUserBidOnItem(new Bid(user1, 10), item1);
bidTracker.recordUserBidOnItem(new Bid(user2, 20), item1);
try { // invalid bid
bidTracker.recordUserBidOnItem(new Bid(user3, 15), item1);
} catch(InvalidBidException e) { /* Silencing the exception as it is irrelevant for this test */ }
bidTracker.recordUserBidOnItem(new Bid(user1, 30), item1);
List<Bid> bidsListOnItem1 = bidTracker.getCurrentAuctionBoardCopy().get(item1);
List<Bid> expectedBidsOnItem1 = Arrays.asList(
new Bid(user1, 10),
new Bid(user2, 20),
new Bid(user1, 30));
assertEquals(expectedBidsOnItem1, bidsListOnItem1);
}
@Test
public void recordUserBidOnItem_shouldOnlyRecordValidBids_inAMultithreadedEnvironment() {
AtomicInteger invalidBidsCount = new AtomicInteger(0);
// Make 10000 bids on 4 different threads.
int totalNbBids = 10000;
ExecutorService executor = Executors.newFixedThreadPool(4);
IntStream.range(0, totalNbBids).forEach(
i -> executor.submit(
() -> {
try {
bidTracker.recordUserBidOnItem(new Bid(user1, i), item1);
} catch(InvalidBidException e) {
invalidBidsCount.incrementAndGet();
}
}
)
);
shutDownExecutor(executor);
List<Bid> actualBidsMade = bidTracker.getCurrentAuctionBoardCopy().get(item1);
// asserting that all bids were processed
assertEquals(totalNbBids, actualBidsMade.size() + invalidBidsCount.get());
// asserting that the accepted bids for the item are all ordered by increasing value
assertEquals(actualBidsMade, sortBidListByValue(actualBidsMade));
// asserting that the last bid is for 9999
Bid lastBidMade = actualBidsMade.get(actualBidsMade.size() - 1);
assertEquals(totalNbBids - 1, lastBidMade.getValue());
}
private void shutDownExecutor(ExecutorService executor) {
try {
executor.awaitTermination(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
System.err.println("tasks interrupted");
} finally {
executor.shutdownNow();
}
}
private List<Bid> sortBidListByValue(List<Bid> bidList) {
return bidList.stream()
.sorted(Comparator.comparing(Bid::getValue))
.collect(Collectors.toList());
}
@Test
public void getCurrentWinningBidForItem_shouldThrowIllegalArgumentException_whenItemIsNull() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Item can't be null");
bidTracker.getCurrentWinningBidForItem(null);
}
@Test
public void getCurrentWinningBidForItem_shouldReturnEmptyOptional_whenItemHasNoBid() {
Optional<Bid> bid = bidTracker.getCurrentWinningBidForItem(item1);
assertEquals(Optional.empty(), bid);
}
@Test
public void getCurrentWinningBidForItem_shouldReturnOptionalWithAValue_whenItemHasBids() throws InvalidBidException {
bidTracker.recordUserBidOnItem(new Bid(user1, 10), item1);
bidTracker.recordUserBidOnItem(new Bid(user2, 20), item1);
Optional<Bid> bid = bidTracker.getCurrentWinningBidForItem(item1);
assertTrue(bid.isPresent());
assertEquals(bid.get(), new Bid(user2, 20));
}
@Test
public void getAllBidsForItem_shouldThrowIllegalArgumentException_whenItemIsNull() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Item can't be null");
bidTracker.getAllBidsForItem(null);
}
@Test
public void getAllBidsForItem_shouldReturnEmptyList_whenItemHasNoBid() {
List<Bid> bids = bidTracker.getAllBidsForItem(item1);
assertTrue(bids.isEmpty());
}
@Test
public void getAllBidsForItem_shouldReturnTheCorrectListOfBids_whenItemHasBids() throws InvalidBidException {
bidTracker.recordUserBidOnItem(new Bid(user1, 10), item1);
bidTracker.recordUserBidOnItem(new Bid(user2, 20), item1);
List<Bid> actualBids = bidTracker.getAllBidsForItem(item1);
List<Bid> expectedBids = Arrays.asList(
new Bid(user1, 10),
new Bid(user2, 20));
assertEquals(expectedBids, actualBids);
}
@Test
public void getAllItemsWithBidFromUser_shouldReturnEmptySet_whenUserIsNull() {
Set<Item> items = bidTracker.getAllItemsWithBidFromUser(null);
assertTrue(items.isEmpty());
}
@Test
public void getAllItemsWithBidFromUser_shouldReturnEmptySet_whenUserHasNoBid() {
Set<Item> items = bidTracker.getAllItemsWithBidFromUser(user1);
assertTrue(items.isEmpty());
}
@Test
public void getAllItemsWithBidFromUser_shouldReturnCorrectItemSet_whenUserHasBids() throws InvalidBidException {
bidTracker.recordUserBidOnItem(new Bid(user1, 10), item1); // bid on item1
bidTracker.recordUserBidOnItem(new Bid(user2, 20), item1);
bidTracker.recordUserBidOnItem(new Bid(user1, 30), item1); // second bid on item1
bidTracker.recordUserBidOnItem(new Bid(user2, 10), item2);
bidTracker.recordUserBidOnItem(new Bid(user3, 20), item2);
bidTracker.recordUserBidOnItem(new Bid(user3, 10), item3);
bidTracker.recordUserBidOnItem(new Bid(user1, 20), item3); // bid on item3
Set<Item> itemList = bidTracker.getAllItemsWithBidFromUser(user1);
Set<Item> expectedItemList = new HashSet<>(Arrays.asList(item1, item3));
assertEquals(expectedItemList, itemList);
}
}